Merge and optimize server modal with setting branch features

This commit is contained in:
2026-04-23 09:47:09 +09:00
18 changed files with 882 additions and 1125 deletions

View File

@@ -49,7 +49,7 @@ export function openDashboardDetail(title: string, list: HardwareAsset[]) {
if (!thead) return;
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 = '';
if (list.length === 0) {
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center; padding: 2rem;">해당 조건의 자산이 없습니다.</td></tr>`;

View File

@@ -1,406 +1,395 @@
import { createIcons, Paperclip, History, Plus, X } from 'lucide';
import { HardwareAsset, HardwareLog } from '../../core/state';
import { state, saveHardwareAsset, deleteHardwareAsset } from '../../core/state';
import { setEditLock } from './ModalUtils';
import { HardwareAsset, HardwareLog } from '../../core/excelHandler';
import { closeModals } from './BaseModal';
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Paperclip } from 'lucide';
import { CORP_LIST, ORG_LIST, HW_TYPE_LIST, LOCATION_DATA, TYPE_PREFIX_MAP } from './SharedData';
import {
generateOptionsHTML,
setFieldValue,
getFieldValue,
parseAndSetLocation,
bindLocationEvents,
getCombinedLocation,
setEditLock,
createModalFrameHTML,
autoFillForm,
autoExtractForm
} from './ModalUtils';
let currentAsset: HardwareAsset | null = null;
let isEditMode = false;
// 공통 옵션 생성 함수
const generateOptionsHTML = (options: string[]) =>
options.map(opt => `<option value="${opt}">${opt}</option>`).join('');
const STATUS_LIST = ['사용중', '보관중', '수리중', '폐기예정', '기타'];
const ORG_LIST = ["경영지원팀", "전략기획팀", "IT운영팀", "공정기술팀", "품질관리팀"];
const HW_TYPE_LIST = ["서버", "데스크탑", "노트북", "워크스테이션", "태블릿", "기타"];
const STATUS_LIST = ["사용중", "유휴", "수리중", "폐기예정", "기타"];
const LOCATION_DATA: Record<string, string[]> = {
"본사": ["1층 전산실", "2층 사무실", "3층 회의실"],
"IDC": ["A구역 랙 01", "A구역 랙 02", "B구역 랙 05"],
"공장": ["생산라인 1", "제어실", "창고"]
// 필드 ID ↔ 데이터 Key 매핑 (서버 전용 필드 포함)
const HW_FIELD_MAP: Record<string, string> = {
'유형': 'type',
'법인': '법인',
'자산코드': '자산코드',
'현사용조직': '실사용조직',
'이전사용조직': '이전사용조직',
'상세용도': '서버용도', // 서버 전용
'모델명': '모델명',
'명칭': '명칭',
'보관위치': '보관위치',
'현재상태': '상태',
'IP주소': 'IP주소',
'IP2': 'IP2', // 서버 전용
'원격접속': '원격방법', // 서버 전용
'서버ID': '서버ID', // 서버 전용
'서버PW': '서버PW', // 서버 전용
'모니터링': '모니터링', // 서버 전용
'OS': 'OS',
'CPU': 'CPU',
'RAM': 'RAM',
'SSD1': 'SSD1',
'SSD2': 'SSD2',
'HW사양': 'HW사양',
'담당자_정': '관리조직',
'구매일': '도입일',
'금액': '구매가',
'비고': '비고',
'사용자': '사용자'
};
const HW_MODAL_HTML = `
<div id="hw-asset-modal" class="modal-overlay hidden">
<div class="modal-content modal-lg">
<div class="modal-header">
<h2 id="hw-modal-title">H/W 자산 정보</h2>
<button id="btn-close-hw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
</div>
<div class="modal-body">
<div class="modal-body-split">
<div class="modal-form-area">
<form id="hw-asset-form" class="grid-form">
<input type="hidden" id="hw-asset-id" />
<input type="hidden" id="hw-asset-type" />
<!-- Group 1: 기본 정보 (Identity) -->
<div class="form-section-title">기본 정보 (Identity)</div>
<div class="form-group">
<label for="hw-자산번호">자산번호</label>
<input type="text" id="hw-자산번호" readonly />
</div>
<div class="form-group">
<label for="hw-관리번호">관리번호/코드</label>
<div style="display:flex; gap:0.5rem;">
<input type="text" id="hw-관리번호" readonly placeholder="자동 생성 또는 직접 입력" required />
<button type="button" id="btn-generate-hw-code" class="btn btn-outline btn-sm hidden" style="white-space:nowrap;">생성</button>
</div>
</div>
<div class="form-group">
<label for="hw-사용자조직">실사용조직</label>
<select id="hw-사용자조직">${generateOptionsHTML(ORG_LIST)}</select>
</div>
<div class="form-group" id="hw-관리조직-group">
<label for="hw-관리조직">자산관리조직</label>
<input type="text" id="hw-관리조직" readonly />
</div>
<div class="form-group">
<label for="hw-자산구분">자산구분</label>
<select id="hw-자산구분">${generateOptionsHTML(HW_TYPE_LIST)}</select>
</div>
<div class="form-group" id="hw-서버용도-group" style="display:none;">
<label for="hw-서버용도">서버용도</label>
<select id="hw-서버용도">
<option value="">선택하세요</option>
<option value="DB서버">DB 서버</option>
<option value="웹서버">웹 서버</option>
<option value="어플리케이션">어플리케이션</option>
<option value="가상화">가상화 호스트</option>
</select>
</div>
<!-- 운영 및 상태 관리 -->
<div class="form-section-title op-only">운영 및 상태 관리</div>
<div class="form-group op-only">
<label for="hw-보관위치">보관위치</label>
<input type="text" id="hw-보관위치" placeholder="예: 7층 비상계단옆 창고" />
</div>
<div class="form-group op-only">
<label for="hw-상태">상태</label>
<select id="hw-상태">${generateOptionsHTML(STATUS_LIST)}</select>
</div>
<!-- 네트워크 및 서버 정보 -->
<div class="form-section-title server-only" id="hw-network-title">네트워크 정보 (Connectivity)</div>
<div class="form-group server-only" id="hw-ip-group">
<label for="hw-IP주소">IP 주소 1</label>
<input type="text" id="hw-IP주소" />
</div>
<div class="form-group server-only" id="hw-ip2-group">
<label for="hw-IP2">IP 주소 2</label>
<input type="text" id="hw-IP2" />
</div>
<div class="form-group server-only" id="hw-원격접속">원격 접속/관리</div>
<div class="form-group server-only" id="hw-remote-group">
<label for="hw-원격방법">원격 방법</label>
<input type="text" id="hw-원격방법" />
</div>
<div class="form-group server-only" id="hw-server-id-group">
<label for="hw-서버ID">서버 ID</label>
<input type="text" id="hw-서버ID" />
</div>
<div class="form-group server-only" id="hw-server-pw-group">
<label for="hw-서버PW">서버 PW</label>
<input type="password" id="hw-서버PW" />
</div>
<div class="form-group non-server" id="hw-ip-non-server-group">
<label for="hw-IP주소-non-server">IP 주소</label>
<input type="text" id="hw-IP주소-non-server" />
</div>
<!-- 하드웨어 사양 -->
<div class="form-section-title" id="hw-spec-title">하드웨어 사양 (Specifications)</div>
<div class="form-group" id="hw-model-group">
<label for="hw-모델명">모델명</label>
<input type="text" id="hw-모델명" />
</div>
<div class="form-group" id="hw-os-group">
<label for="hw-OS">운영체제 (OS)</label>
<input type="text" id="hw-OS" />
</div>
<div class="form-group" id="hw-cpu-group">
<label for="hw-CPU">CPU 사양</label>
<input type="text" id="hw-CPU" />
</div>
<div class="form-group" id="hw-ram-group">
<label for="hw-RAM">RAM 용량</label>
<input type="text" id="hw-RAM" />
</div>
<div class="form-group" id="hw-ssd1-group">
<label for="hw-SSD1">Storage 1 (SSD/HDD)</label>
<input type="text" id="hw-SSD1" />
</div>
<div class="form-group" id="hw-ssd2-group">
<label for="hw-SSD2">Storage 2 (SSD/HDD)</label>
<input type="text" id="hw-SSD2" />
</div>
<div class="form-group server-only" id="hw-monitoring-group">
<label for="hw-모니터링">모니터링 유무</label>
<input type="text" id="hw-모니터링" />
</div>
<div class="form-group full-width non-server" id="hw-hwspec-group">
<label for="hw-HW사양">사양 상세</label>
<textarea id="hw-HW사양" rows="2"></textarea>
</div>
<!-- 설치/위치 관리 -->
<div class="form-section-title" id="hw-op-title">설치 및 위치 관리</div>
<div class="form-group loc-standard">
<label for="hw-설치장소">설치장소(대분류)</label>
<select id="hw-설치장소">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
</div>
<div class="form-group loc-standard">
<label for="hw-세부위치">세부 설치 위치</label>
<select id="hw-세부위치"><option value="">선택하세요</option></select>
</div>
<div class="form-group" id="hw-위치기타-group" style="display:none;">
<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-구매가" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
</div>
<div class="form-group full-width">
<label for="hw-비고">비고</label>
<textarea id="hw-비고" rows="2"></textarea>
</div>
</form>
</div>
<div class="modal-history-area">
<div class="history-header">
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 유지보수 및 변경 이력</h3>
<button type="button" id="btn-add-hw-log" class="btn btn-outline btn-sm">
이력 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
</button>
</div>
<div id="hw-history-list" class="history-timeline"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button id="btn-delete-hw-asset" class="btn btn-outline btn-danger">삭제</button>
<div class="footer-actions">
<button id="btn-revert-hw-edit" class="btn btn-outline hidden">취소</button>
<button id="btn-save-hw-asset" class="btn btn-primary">수정</button>
<button id="btn-close-modal-bottom" class="btn btn-outline">닫기</button>
</div>
</div>
const HW_FORM_HTML = `
<!-- Group 1: 기본 정보 -->
<div class="form-section-title">기본 정보 (Identity)</div>
<div class="form-group">
<label for="hw-법인">구매법인</label>
<select id="hw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
</div>
<div class="form-group">
<label for="hw-자산코드">자산번호</label>
<div class="input-with-btn">
<input type="text" id="hw-자산코드" readonly class="is-readonly-field" placeholder="번호 생성을 클릭하세요" required />
<button type="button" id="btn-generate-hw-code" class="btn btn-outline btn-sm">생성</button>
</div>
</div>
<!-- 이력 추가 모달 -->
<div id="hw-log-modal" class="modal-overlay hidden" style="z-index: 1100;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h2>이력 추가</h2>
<button id="btn-close-hw-log" class="btn-icon"><i data-lucide="x"></i></button>
</div>
<div class="modal-body">
<div class="grid-form" style="grid-template-columns: 1fr;">
<div class="form-group">
<label>일자</label>
<input type="date" id="new-hw-log-date" />
</div>
<div class="form-group">
<label>변경/유지보수 내용</label>
<textarea id="new-hw-log-details" rows="3" placeholder="예: [유지보수] 먼지 제거, [부품교체] 배터리 교체 완료"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<div></div>
<div class="footer-actions">
<button id="btn-cancel-hw-log" class="btn btn-outline">취소</button>
<button id="btn-confirm-hw-log" class="btn btn-primary">추가</button>
</div>
</div>
</div>
<div class="form-group pc-only">
<label for="hw-사용자">사용자</label>
<input type="text" id="hw-사용자" />
</div>
<div class="form-group">
<label for="hw-현사용조직">실사용조직</label>
<select id="hw-현사용조직">${generateOptionsHTML(ORG_LIST)}</select>
</div>
<div class="form-group" id="hw-이전사용조직-group">
<label for="hw-이전사용조직">이전 사용조직</label>
<input type="text" id="hw-이전사용조직" readonly />
</div>
<div class="form-group" id="hw-유형-group">
<label for="hw-유형">자산구분</label>
<select id="hw-유형">${generateOptionsHTML(HW_TYPE_LIST)}</select>
</div>
<div class="form-group" id="hw-상세용도-group" style="display:none;">
<label for="hw-상세용도">서버용도</label>
<select id="hw-상세용도">
<option value="">선택하세요</option>
<option value="DB서버">DB 서버</option>
<option value="웹서버">웹 서버</option>
<option value="어플리케이션">어플리케이션</option>
<option value="가상화">가상화 호스트</option>
</select>
</div>
<div class="form-section-title op-only" id="hw-op-title">운영 및 상태 관리</div>
<div class="form-group op-only">
<label for="hw-보관위치">보관위치</label>
<input type="text" id="hw-보관위치" placeholder="예: 7층 비품창고" />
</div>
<div class="form-group op-only">
<label for="hw-현재상태">현재상태</label>
<select id="hw-현재상태">${generateOptionsHTML(STATUS_LIST)}</select>
</div>
<div class="form-section-title server-only" id="hw-network-title">네트워크 정보 (Connectivity)</div>
<div class="form-group server-only" id="hw-ip-group"><label for="hw-IP주소">IP 주소 1</label><input type="text" id="hw-IP주소" /></div>
<div class="form-group server-only" id="hw-ip2-group"><label for="hw-IP2">IP 주소 2</label><input type="text" id="hw-IP2" /></div>
<div class="form-group server-only" id="hw-remote-group"><label for="hw-원격접속">원격 방법</label><input type="text" id="hw-원격접속" /></div>
<div class="form-group server-only" id="hw-server-id-group"><label for="hw-서버ID">서버 ID</label><input type="text" id="hw-서버ID" /></div>
<div class="form-group server-only" id="hw-server-pw-group"><label for="hw-서버PW">서버 PW</label><input type="password" id="hw-서버PW" /></div>
<div class="form-group non-server" id="hw-ip-non-server-group"><label for="hw-IP주소-non-server">IP 주소</label><input type="text" id="hw-IP주소-non-server" /></div>
<div class="form-section-title" id="hw-spec-title">하드웨어 사양 (Specifications)</div>
<div class="form-group" id="hw-model-group"><label for="hw-모델명">모델명</label><input type="text" id="hw-모델명" /></div>
<div class="form-group" id="hw-os-group"><label for="hw-OS">운영체제 (OS)</label><input type="text" id="hw-OS" /></div>
<div class="form-group" id="hw-cpu-group"><label for="hw-CPU">CPU 사양</label><input type="text" id="hw-CPU" /></div>
<div class="form-group" id="hw-ram-group"><label for="hw-RAM">RAM 용량</label><input type="text" id="hw-RAM" /></div>
<div class="form-group" id="hw-ssd1-group"><label for="hw-SSD1">Storage 1 (SSD/HDD)</label><input type="text" id="hw-SSD1" /></div>
<div class="form-group" id="hw-ssd2-group"><label for="hw-SSD2">Storage 2 (SSD/HDD)</label><input type="text" id="hw-SSD2" /></div>
<div class="form-group server-only" id="hw-monitoring-group"><label for="hw-모니터링">모니터링 유무</label><input type="text" id="hw-모니터링" /></div>
<div class="form-group full-width non-server" id="hw-hwspec-group"><label for="hw-HW사양">사양 상세</label><textarea id="hw-HW사양" rows="2"></textarea></div>
<div class="form-section-title" id="hw-loc-title">설치 위치 및 관리</div>
<div class="form-group loc-standard"><label for="hw-위치-빌딩">설치장소 (대분류)</label><select id="hw-위치-빌딩">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select></div>
<div class="form-group loc-standard"><label for="hw-위치-상세">세부 설치 위치</label><select id="hw-위치-상세"><option value="">선택</option></select></div>
<div class="form-group" id="hw-위치-기타-group" style="display:none;"><label for="hw-위치-기타">직접 입력 (기타)</label><input type="text" id="hw-위치-기타" /></div>
<div class="form-group"><label for="hw-담당자_정">자산관리조직</label><input type="text" id="hw-담당자_정" readonly /></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-금액" oninput="this.value=this.value.replace(/[^0-9]/g,'').replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g,',')" /></div>
<div class="form-group full-width"><label for="hw-비고">비고</label><textarea id="hw-비고" rows="2"></textarea></div>
`;
function renderHwHistory(assetId: string) {
const container = document.getElementById('hw-history-list');
if (!container) return;
const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId);
if (logs.length === 0) {
container.innerHTML = '<div class="empty-history">기록이 없습니다.</div>';
return;
}
const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId).sort((a,b) => new Date(b.date).getTime() - new Date(a.date).getTime());
if (logs.length === 0) { container.innerHTML = '<div class="empty-history">기록된 이력이 없습니다.</div>'; return; }
container.innerHTML = logs.map(l => `
<div class="history-item">
<div class="history-date">${l.date}</div>
<div class="history-user">${l.user}</div>
<div class="history-details">${l.details}</div>
<div class="history-details">${l.details.replace(/\n/g, '<br>')}</div>
</div>
`).join('');
}
function getFieldValue(id: string): string {
return (document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement)?.value || '';
}
function applyTypeSpecificUI(type: string) {
const detailPurpose = getFieldValue('hw-서버용도');
const detailPurpose = getFieldValue('hw-상세용도');
const upperType = (type || '').toUpperCase();
const groups: Record<string, HTMLElement | null> = {
detailPurpose: document.getElementById('hw-서버용도-group'),
detailPurpose: document.getElementById('hw-상세용도-group'),
networkTitle: document.getElementById('hw-network-title'),
specTitle: document.getElementById('hw-spec-title'),
opTitle: document.getElementById('hw-op-title')
opTitle: document.getElementById('hw-op-title'),
model: document.getElementById('hw-model-group'),
os: document.getElementById('hw-os-group'),
cpu: document.getElementById('hw-cpu-group'),
ram: document.getElementById('hw-ram-group'),
ssd1: document.getElementById('hw-ssd1-group'),
ssd2: document.getElementById('hw-ssd2-group'),
hwSpec: document.getElementById('hw-hwspec-group'),
monitoring: document.getElementById('hw-monitoring-group'),
user: document.querySelector('.pc-only') as HTMLElement
};
const isServer = upperType.includes('서버') || upperType.includes('SERVER');
const serverOnly = document.querySelectorAll('.server-only');
const nonServer = document.querySelectorAll('.non-server');
const opOnly = document.querySelectorAll('.op-only');
const standardLoc = document.querySelectorAll('.loc-standard');
serverOnly.forEach(el => (el as HTMLElement).style.display = isServer ? 'block' : 'none');
nonServer.forEach(el => (el as HTMLElement).style.display = isServer ? 'none' : 'block');
// 초기화
serverOnly.forEach(el => (el as HTMLElement).style.display = 'none');
nonServer.forEach(el => (el as HTMLElement).style.display = 'none');
opOnly.forEach(el => (el as HTMLElement).style.display = 'none');
standardLoc.forEach(el => (el as HTMLElement).style.display = 'flex');
Object.values(groups).forEach(g => { if (g) g.style.display = 'none'; });
if (groups.detailPurpose) groups.detailPurpose.style.display = isServer ? 'block' : 'none';
const isServer = upperType.includes('서버') || upperType.includes('SERVER');
const isMobileGroup = ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t));
const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품');
const isOpType = isMobileGroup || isEquipGroup;
const isPcType = upperType === 'PC' || upperType === '개인PC' || upperType === '노트북';
if (isServer) {
serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
if (groups.detailPurpose) groups.detailPurpose.style.display = 'flex';
if (groups.networkTitle) groups.networkTitle.style.display = 'flex';
if (groups.specTitle) groups.specTitle.style.display = 'flex';
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
} else if (isPcType) {
if (groups.user) groups.user.style.display = 'flex';
if (groups.specTitle) groups.specTitle.style.display = 'flex';
nonServer.forEach(el => (el as HTMLElement).style.display = 'flex');
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
} else if (isOpType) {
opOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
standardLoc.forEach(el => (el as HTMLElement).style.display = 'none');
if (groups.specTitle) groups.specTitle.style.display = 'flex';
if (groups.model) groups.model.style.display = 'flex';
if (groups.hwSpec) groups.hwSpec.style.display = 'flex';
} else {
// 기본값: 전체 필드 표시
nonServer.forEach(el => (el as HTMLElement).style.display = 'flex');
if (groups.specTitle) groups.specTitle.style.display = 'flex';
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec'].forEach(k => { if (groups[k]) groups[k]!.style.display = 'flex'; });
}
}
export function openHwModal(asset: any) {
export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view') {
currentAsset = asset;
isEditMode = false;
const modal = document.getElementById('hw-asset-modal')!;
const form = document.getElementById('hw-asset-form') as HTMLFormElement;
form.reset();
fillHwFormData(asset);
renderHwHistory(asset.id);
setEditLock('hw-asset-form', mode, {
saveBtnId: 'btn-save-hw-asset',
revertBtnId: 'btn-revert-hw-edit',
generateBtnId: 'btn-generate-hw-code',
addLogBtnId: 'btn-add-hw-log'
});
isEditMode = (mode === 'add');
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
// 데이터 채우기 (자동 매핑)
autoFillForm('hw', asset, HW_FIELD_MAP);
// 위치 정보 처리
parseAndSetLocation(asset., 'hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타');
modal.classList.remove('hidden');
createIcons({ icons: { Paperclip, History, Plus, X } });
}
function fillHwFormData(asset: any) {
const setVal = (id: string, val: any) => {
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
if (el) el.value = val || '';
};
setVal('hw-asset-id', asset.id);
setVal('hw-asset-type', asset.type);
setVal('hw-자산번호', asset. || '');
setVal('hw-관리번호', asset. || asset. || '');
setVal('hw-사용자조직', asset. || '');
setVal('hw-관리조직', asset. || '전산팀');
setVal('hw-자산구분', asset. || asset.type || '');
setVal('hw-서버용도', asset. || '');
setVal('hw-보관위치', asset. || '');
setVal('hw-상태', asset. || '사용중');
setVal('hw-IP주소', asset.IP주소 || '');
setVal('hw-IP2', asset.IP2 || '');
setVal('hw-원격방법', asset. || '');
setVal('hw-서버ID', asset.ID || '');
setVal('hw-서버PW', asset.PW || '');
setVal('hw-IP주소-non-server', asset.IP주소 || '');
setVal('hw-모델명', asset. || '');
setVal('hw-OS', asset.OS || '');
setVal('hw-CPU', asset.CPU || '');
setVal('hw-RAM', asset.RAM || '');
setVal('hw-SSD1', asset.SSD1 || '');
setVal('hw-SSD2', asset.SSD2 || '');
setVal('hw-모니터링', asset. || '');
setVal('hw-HW사양', asset.HW사양 || '');
setVal('hw-설치장소', asset. || '');
setVal('hw-도입일', asset. || '');
setVal('hw-구매가', asset. || '');
setVal('hw-비고', asset. || '');
applyTypeSpecificUI(asset.type);
renderHwHistory(asset.id);
modal.classList.remove('hidden');
createIcons({ icons: { X, Save, Edit2, RotateCcw, History, Plus, Paperclip } });
}
export function initHwModal(onSave: () => void) {
document.body.insertAdjacentHTML('beforeend', HW_MODAL_HTML);
export function initHwModal(onSave: () => void, closeModalsCb: () => void) {
if (!document.getElementById('hw-asset-modal')) {
const html = createModalFrameHTML('hw', '자산 상세 정보', HW_FORM_HTML, {
historyTitle: '유지보수 및 변경 이력',
addLogBtnId: 'btn-add-hw-log'
});
document.body.insertAdjacentHTML('beforeend', html);
// 이력 추가 모달
const logModalHTML = `
<div id="hw-log-modal" class="modal-overlay hidden" style="z-index: 1100;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header"><h2>이력 추가</h2><button id="btn-close-hw-log" class="btn-icon"><i data-lucide="x"></i></button></div>
<div class="modal-body">
<div class="grid-form" style="grid-template-columns: 1fr;">
<div class="form-group"><label>날짜</label><input type="date" id="new-hw-log-date" /></div>
<div class="form-group"><label>내용</label><textarea id="new-hw-log-details" rows="3" placeholder="예: [유지보수] 먼지 제거, [부품교체] 배터리 교체 등"></textarea></div>
</div>
</div>
<div class="modal-footer"><div></div><div class="footer-actions"><button id="btn-cancel-hw-log" class="btn btn-outline">취소</button><button id="btn-confirm-hw-log" class="btn btn-primary">추가</button></div></div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', logModalHTML);
}
const modal = document.getElementById('hw-asset-modal')!;
const closeBtn = document.getElementById('btn-close-hw-modal')!;
const closeBtnBottom = document.getElementById('btn-close-modal-bottom')!;
const form = document.getElementById('hw-asset-form') as HTMLFormElement;
const saveBtn = document.getElementById('btn-save-hw-asset')!;
const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
const revertBtn = document.getElementById('btn-revert-hw-edit')!;
const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
const typeSelect = document.getElementById('hw-유형') as HTMLSelectElement;
const detailPurposeSelect = document.getElementById('hw-상세용도') as HTMLSelectElement;
const logAddBtn = document.getElementById('btn-add-hw-log')!;
const logModal = document.getElementById('hw-log-modal')!;
const closeModalAction = () => {
modal.classList.add('hidden');
[typeSelect, detailPurposeSelect].forEach(el => {
el?.addEventListener('change', () => applyTypeSpecificUI(typeSelect.value));
});
bindLocationEvents('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타');
const closeModalAction = () => { closeModalsCb(); isEditMode = false; };
document.getElementById('btn-close-hw-modal')?.addEventListener('click', closeModalAction);
document.getElementById('btn-cancel-hw-modal')?.addEventListener('click', closeModalAction);
revertBtn.addEventListener('click', () => {
setEditLock('hw-asset-form', 'view', {
saveBtnId: 'btn-save-hw-asset',
revertBtnId: 'btn-revert-hw-edit',
generateBtnId: 'btn-generate-hw-code',
addLogBtnId: 'btn-add-hw-log'
});
isEditMode = false;
};
if (currentAsset) openHwModal(currentAsset, 'view');
});
closeBtn.addEventListener('click', closeModalAction);
closeBtnBottom.addEventListener('click', closeModalAction);
document.getElementById('btn-generate-hw-code')?.addEventListener('click', async () => {
const typeValue = typeSelect.value;
const purchaseDate = getFieldValue('hw-구매일');
const typeCode = TYPE_PREFIX_MAP[typeValue] || 'ETC';
const dateStr = purchaseDate.replace(/[^0-9]/g, '');
if (dateStr.length < 6) { alert('올바른 도입일(YYYYMM)을 입력해주세요.'); return; }
const prefix = `${typeCode}-${dateStr.substring(0, 6)}-`;
try {
const res = await fetch(`http://localhost:3000/api/generate-asset-code?prefix=${prefix}`);
const data = await res.json();
if (data.nextCode) setFieldValue('hw-자산코드', data.nextCode);
} catch (err) { alert('자산번호 생성에 실패했습니다.'); }
});
saveBtn.addEventListener('click', () => {
if (!currentAsset) return;
if (!isEditMode) {
setEditLock('hw-asset-form', 'edit', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
setEditLock('hw-asset-form', 'edit', {
saveBtnId: 'btn-save-hw-asset',
revertBtnId: 'btn-revert-hw-edit',
generateBtnId: 'btn-generate-hw-code',
addLogBtnId: 'btn-add-hw-log'
});
isEditMode = true;
applyTypeSpecificUI(getFieldValue('hw-유형'));
return;
}
// 데이터 추출 (자동 매핑)
const extracted = autoExtractForm('hw', HW_FIELD_MAP);
if (!extracted.) {
alert('자산번호가 없습니다. [생성] 버튼을 눌러 자산번호를 부여해주세요.');
return;
}
// 변경 이력 자동 생성
const diffLogs: string[] = [];
const compareFields = [
{ key: '실사용조직', label: '실사용조직' },
{ key: '위치', label: '위치' },
{ key: '상태', label: '상태' },
{ key: 'IP주소', label: 'IP' }
];
const currentLocation = currentAsset. || '';
const newLocation = getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타');
compareFields.forEach(f => {
let oldVal = (currentAsset as any)[f.key] || '';
let newVal = (extracted as any)[f.key] || '';
if (f.key === '위치') { oldVal = currentLocation; newVal = newLocation; }
if (f.key === 'IP주소') { newVal = getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'); }
if (oldVal !== newVal) {
diffLogs.push(`${f.label}: ${oldVal || '(없음)'}${newVal || '(없음)'}`);
}
});
if (diffLogs.length > 0) {
state.masterData.logs = state.masterData.logs || [];
state.masterData.logs.push({
id: Math.random().toString(36).substring(2, 9),
assetId: currentAsset.id,
date: new Date().toISOString().split('T')[0],
user: '관리자',
details: diffLogs.join('\n')
});
}
const updated: any = {
...currentAsset,
관리번호: getFieldValue('hw-관리번호'),
실사용조직: getFieldValue('hw-사용자조직'),
자산구분: getFieldValue('hw-자산구분'),
서버용도: getFieldValue('hw-서버용도'),
보관위치: getFieldValue('hw-보관위치'),
상태: getFieldValue('hw-상태'),
...extracted,
IP주소: getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'),
IP2: getFieldValue('hw-IP2'),
원격방법: getFieldValue('hw-원격방법'),
서버ID: getFieldValue('hw-서버ID'),
서버PW: getFieldValue('hw-서버PW'),
모델명: getFieldValue('hw-모델명'),
OS: getFieldValue('hw-OS'),
CPU: getFieldValue('hw-CPU'),
RAM: getFieldValue('hw-RAM'),
SSD1: getFieldValue('hw-SSD1'),
SSD2: getFieldValue('hw-SSD2'),
모니터링: getFieldValue('hw-모니터링'),
HW사양: getFieldValue('hw-HW사양'),
설치장소: getFieldValue('hw-설치장소'),
도입일: getFieldValue('hw-도입일'),
구매가: getFieldValue('hw-구매가'),
비고: getFieldValue('hw-비고')
위치: newLocation
};
saveHardwareAsset(updated);
onSave();
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
setEditLock('hw-asset-form', 'view', {
saveBtnId: 'btn-save-hw-asset',
revertBtnId: 'btn-revert-hw-edit',
generateBtnId: 'btn-generate-hw-code',
addLogBtnId: 'btn-add-hw-log'
});
isEditMode = false;
});
deleteBtn.addEventListener('click', () => {
if (currentAsset && confirm('정말 삭제하시겠습니까?')) {
if (currentAsset && confirm('정말 삭제하시겠습니까?')) {
deleteHardwareAsset(currentAsset.id);
onSave();
closeModalAction();
}
});
revertBtn.addEventListener('click', () => {
if (currentAsset) {
fillHwFormData(currentAsset);
setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
isEditMode = false;
}
});
// 이력 추가 관련 이벤트
const logModal = document.getElementById('hw-log-modal')!;
document.getElementById('btn-add-hw-log')?.addEventListener('click', () => {
logAddBtn.addEventListener('click', () => {
logModal.classList.remove('hidden');
(document.getElementById('new-hw-log-date') as HTMLInputElement).value = new Date().toISOString().split('T')[0];
(document.getElementById('new-hw-log-details') as HTMLTextAreaElement).value = '';
logModal.classList.remove('hidden');
});
document.getElementById('btn-close-hw-log')?.addEventListener('click', () => logModal.classList.add('hidden'));
@@ -409,23 +398,10 @@ export function initHwModal(onSave: () => void) {
if (!currentAsset) return;
const date = (document.getElementById('new-hw-log-date') as HTMLInputElement).value;
const details = (document.getElementById('new-hw-log-details') as HTMLTextAreaElement).value;
if (!details) {
alert('내용을 입력해주세요.');
return;
}
const newLog: HardwareLog = {
id: Math.random().toString(36).substring(2, 9),
assetId: currentAsset.id,
date,
details,
user: '관리자'
};
if (!state.masterData.logs) state.masterData.logs = [];
state.masterData.logs.unshift(newLog);
renderHwHistory(currentAsset.id);
if (!date || !details) return;
state.masterData.logs = state.masterData.logs || [];
state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: currentAsset.id, date, user: '관리자', details });
logModal.classList.add('hidden');
renderHwHistory(currentAsset.id);
});
}

View File

@@ -103,13 +103,15 @@ export function setEditLock(
options: {
saveBtnId: string,
revertBtnId: string,
generateBtnId?: string
generateBtnId?: string,
addLogBtnId?: string
}
) {
const form = document.getElementById(formId) as HTMLFormElement;
const saveBtn = document.getElementById(options.saveBtnId);
const revertBtn = document.getElementById(options.revertBtnId);
const generateBtn = options.generateBtnId ? document.getElementById(options.generateBtnId) : null;
const addLogBtn = options.addLogBtnId ? document.getElementById(options.addLogBtnId) : null;
if (!form || !saveBtn || !revertBtn) return;
@@ -118,10 +120,14 @@ export function setEditLock(
form.classList.remove('is-view-mode');
form.classList.add('is-edit-mode');
saveBtn.textContent = '저장';
revertBtn.classList.toggle('hidden', mode === 'add'); // 신규 추가 시에는 취소 버튼 숨김 (닫기가 대신함)
revertBtn.classList.toggle('hidden', mode === 'add'); // 신규 추가 시에는 취소 버튼 숨김
// 번호 생성 버튼은 '추가' 시에만 노출
if (generateBtn) generateBtn.classList.toggle('hidden', mode !== 'add');
// 번호 생성 버튼은 '추가(add)' 시에만 노출
if (generateBtn) {
generateBtn.style.display = mode === 'add' ? 'flex' : 'none';
}
// 내역 추가 버튼 노출
if (addLogBtn) addLogBtn.style.display = 'flex';
} else {
// 조회 모드 (잠금)
form.classList.remove('is-edit-mode');
@@ -129,7 +135,79 @@ export function setEditLock(
saveBtn.textContent = '수정';
revertBtn.classList.add('hidden');
// 조회 모드에서는 번호 생성 버튼 무조건 숨김
if (generateBtn) generateBtn.classList.add('hidden');
// 조회 모드에서는 버튼들 숨김
if (generateBtn) generateBtn.style.display = 'none';
if (addLogBtn) addLogBtn.style.display = 'none';
}
}
/**
* 8. 공통 모달 프레임 템플릿 생성
* @param idPrefix 필드 ID의 접두사 (예: 'hw', 'sw', 'pc')
* @param title 모달 제목
* @param formContent 각 모달마다 다른 폼 본문 HTML
* @param options 설정 (이력 영역 제목 등)
*/
export function createModalFrameHTML(
idPrefix: string,
title: string,
formContent: string,
options: { historyTitle: string, addLogBtnId: string }
): string {
return `
<div id="${idPrefix}-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide">
<div class="modal-header">
<h2 id="${idPrefix}-modal-title">${title}</h2>
<button id="btn-close-${idPrefix}-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
</div>
<div class="modal-body">
<div class="modal-body-split">
<div class="modal-form-area">
<form id="${idPrefix}-asset-form" class="grid-form">
<input type="hidden" id="${idPrefix}-asset-id" />
<input type="hidden" id="${idPrefix}-asset-type" />
${formContent}
</form>
</div>
<div class="modal-history-area">
<div class="history-header">
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> ${options.historyTitle}</h3>
<button type="button" id="${options.addLogBtnId}" class="btn btn-outline btn-sm">
내역 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
</button>
</div>
<div id="${idPrefix}-history-list" class="history-timeline"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button id="btn-delete-${idPrefix}-asset" class="btn btn-outline btn-danger">삭제</button>
<div class="footer-actions">
<button id="btn-revert-${idPrefix}-edit" class="btn btn-outline hidden">수정 취소</button>
<button id="btn-cancel-${idPrefix}-modal" class="btn btn-outline">닫기</button>
<button id="btn-save-${idPrefix}-asset" class="btn btn-primary">수정</button>
</div>
</div>
</div>
</div>
`;
}
/**
* 9. 데이터 ↔ 폼 자동 매핑 (유지보수 핵심)
*/
export function autoFillForm(idPrefix: string, data: any, fieldMap: Record<string, string>) {
Object.entries(fieldMap).forEach(([fieldId, dataKey]) => {
setFieldValue(`${idPrefix}-${fieldId}`, data[dataKey]);
});
}
export function autoExtractForm(idPrefix: string, fieldMap: Record<string, string>): any {
const result: any = {};
Object.entries(fieldMap).forEach(([fieldId, dataKey]) => {
result[dataKey] = getFieldValue(`${idPrefix}-${fieldId}`);
});
return result;
}

View File

@@ -1,362 +0,0 @@
import { state, saveHardwareAsset, deleteHardwareAsset } from '../../core/state';
import { HardwareAsset } from '../../core/excelHandler';
import { openModal, closeModals } from './BaseModal';
import { createIcons, History, X, Paperclip } from 'lucide';
import { CORP_LIST, ORG_LIST, HW_TYPE_LIST, LOCATION_DATA } from './SharedData';
import {
generateOptionsHTML,
setFieldValue,
getFieldValue,
parseAndSetLocation,
bindLocationEvents,
getCombinedLocation
} from './ModalUtils';
let currentAsset: HardwareAsset | null = null;
let isEditMode = false;
const PC_MODAL_HTML = `
<div id="pc-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide">
<div class="modal-header">
<h2 id="pc-modal-title">개인PC 상세 정보</h2>
<button id="btn-close-pc-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
</div>
<div class="modal-body">
<div class="modal-body-split">
<div class="modal-form-area">
<form id="pc-asset-form" class="grid-form">
<input type="hidden" id="pc-asset-id" />
<input type="hidden" id="pc-asset-type" value="개인PC" />
<div class="form-section-title">기본 정보 (Identity)</div>
<div class="form-group">
<label for="pc-법인">구매법인</label>
<select id="pc-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
</div>
<div class="form-group">
<label for="pc-자산코드">자산번호/코드</label>
<input type="text" id="pc-자산코드" readonly placeholder="자동 생성됩니다" required />
</div>
<div class="form-group">
<label for="pc-유형">유형</label>
<select id="pc-유형">${generateOptionsHTML(HW_TYPE_LIST)}</select>
</div>
<div class="form-group">
<label for="pc-상세용도">상세용도</label>
<select id="pc-상세용도">
<option value="개인PC">개인PC</option>
<option value="서버">서버</option>
</select>
</div>
<div class="form-group">
<label for="pc-사용자">사용자</label>
<input type="text" id="pc-사용자" required />
</div>
<div class="form-group">
<label for="pc-현사용조직">현 사용조직</label>
<select id="pc-현사용조직">${generateOptionsHTML(ORG_LIST)}</select>
</div>
<div class="form-group" id="pc-이전사용조직-group">
<label for="pc-이전사용조직">이전 사용조직</label>
<input type="text" id="pc-이전사용조직" readonly />
</div>
<div class="form-section-title">시스템 사양 (Specifications)</div>
<div class="form-group">
<label for="pc-모델명">모델명</label>
<input type="text" id="pc-모델명" />
</div>
<div class="form-group">
<label for="pc-OS">운영체제 (OS)</label>
<input type="text" id="pc-OS" />
</div>
<div class="form-group">
<label for="pc-CPU">CPU 사양</label>
<input type="text" id="pc-CPU" />
</div>
<div class="form-group">
<label for="pc-RAM">RAM 용량</label>
<input type="text" id="pc-RAM" />
</div>
<div class="form-group">
<label for="pc-SSD1">Storage 1 (SSD/HDD)</label>
<input type="text" id="pc-SSD1" />
</div>
<div class="form-group">
<label for="pc-SSD2">Storage 2 (SSD/HDD)</label>
<input type="text" id="pc-SSD2" />
</div>
<div class="form-section-title" id="pc-location-title">관리 및 운영 (Operation)</div>
<div class="form-group pc-location-field">
<label for="pc-위치-빌딩">설치위치 (건물)</label>
<select id="pc-위치-빌딩">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
</div>
<div class="form-group pc-location-field">
<label for="pc-위치-상세">상세 위치</label>
<select id="pc-위치-상세">
<option value="">건물을 먼저 선택하세요</option>
</select>
</div>
<div class="form-group" id="pc-위치-기타-group" style="display:none;">
<label for="pc-위치-기타">직접 입력 (기타)</label>
<input type="text" id="pc-위치-기타" />
</div>
<div class="form-group">
<label for="pc-구매일">구매일</label>
<input type="text" id="pc-구매일" />
</div>
<div class="form-group">
<label for="pc-금액">금액</label>
<input type="text" id="pc-금액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\\\B(?=(\\\\d{3})+(?!\d))/g, ',')" />
</div>
<div class="form-group">
<label for="pc-납품업체">납품업체</label>
<input type="text" id="pc-납품업체" />
</div>
<div class="form-group full-width">
<label>품의서 (파일)</label>
<div style="display:flex; align-items:center; gap:0.5rem;">
<input type="file" id="pc-품의서" />
<span id="pc-품의서명" style="font-size:0.75rem; color:var(--text-light)"></span>
</div>
</div>
</form>
</div>
<div class="modal-history-area">
<div class="history-header">
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 수정 이력</h3>
</div>
<div id="pc-history-list" class="history-timeline">
<div class="empty-history">이력이 없습니다.</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button id="btn-delete-pc-asset" class="btn btn-outline btn-danger">삭제</button>
<div class="footer-actions">
<button id="btn-revert-pc-edit" class="btn btn-outline hidden">수정 취소</button>
<button id="btn-cancel-pc-modal" class="btn btn-outline">닫기</button>
<button id="btn-save-pc-asset" class="btn btn-primary">수정</button>
</div>
</div>
</div>
</div>
`;
export function openPcModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view') {
currentAsset = asset;
const modal = document.getElementById('pc-asset-modal');
if (!modal) return;
const form = document.getElementById('pc-asset-form') as HTMLFormElement;
const saveBtn = document.getElementById('btn-save-pc-asset')!;
const revertBtn = document.getElementById('btn-revert-pc-edit')!;
if (form) form.reset();
if (mode === 'add') {
isEditMode = true;
if (form) {
form.classList.remove('is-view-mode');
form.classList.add('is-edit-mode');
}
saveBtn.textContent = '저장';
revertBtn.classList.add('hidden');
const prevOrgGroup = document.getElementById('pc-이전사용조직-group');
if (prevOrgGroup) prevOrgGroup.style.display = 'none';
} else {
isEditMode = false;
if (form) {
form.classList.remove('is-edit-mode');
form.classList.add('is-view-mode');
}
saveBtn.textContent = '수정';
revertBtn.classList.add('hidden');
const prevOrgGroup = document.getElementById('pc-이전사용조직-group');
if (prevOrgGroup) prevOrgGroup.style.display = 'flex';
}
fillFormData(asset);
renderHistory(asset.id);
modal.classList.remove('hidden');
applyPcTypeSpecificUI();
createIcons({ icons: { X, History, Paperclip } });
}
function applyPcTypeSpecificUI() {
const type = getFieldValue('pc-유형');
const detailPurpose = getFieldValue('pc-상세용도');
const modelGroup = document.getElementById('pc-모델명')?.closest('.form-group') as HTMLElement;
const osGroup = document.getElementById('pc-OS')?.closest('.form-group') as HTMLElement;
const cpuGroup = document.getElementById('pc-CPU')?.closest('.form-group') as HTMLElement;
const ramGroup = document.getElementById('pc-RAM')?.closest('.form-group') as HTMLElement;
const ssd1Group = document.getElementById('pc-SSD1')?.closest('.form-group') as HTMLElement;
const ssd2Group = document.getElementById('pc-SSD2')?.closest('.form-group') as HTMLElement;
const locationFields = document.querySelectorAll('.pc-location-field');
const etcGroup = document.getElementById('pc-위치-기타-group');
// 초기화 (숨김)
[modelGroup, osGroup, cpuGroup, ramGroup, ssd1Group, ssd2Group].forEach(g => { if(g) g.style.display = 'none'; });
locationFields.forEach(el => (el as HTMLElement).style.display = 'none');
if (etcGroup) etcGroup.style.display = 'none';
if (type === '서버') {
[modelGroup, osGroup, cpuGroup, ramGroup, ssd1Group, ssd2Group].forEach(g => { if(g) g.style.display = 'flex'; });
locationFields.forEach(el => (el as HTMLElement).style.display = 'flex');
}
else if (['스토리지', 'NAS', 'DAS'].includes(type)) {
[modelGroup, ssd1Group, ssd2Group].forEach(g => { if(g) g.style.display = 'flex'; });
locationFields.forEach(el => (el as HTMLElement).style.display = 'flex');
}
else if (type === 'PC' || type === '노트북') {
[modelGroup, osGroup, cpuGroup, ramGroup, ssd1Group, ssd2Group].forEach(g => { if(g) g.style.display = 'flex'; });
if (detailPurpose === '서버') {
locationFields.forEach(el => (el as HTMLElement).style.display = 'flex');
}
}
else if (['CPU', 'GPU', '모바일'].includes(type)) {
if (modelGroup) modelGroup.style.display = 'flex';
}
else if (type === 'RAM') {
if (ramGroup) ramGroup.style.display = 'flex';
}
else if (type === 'HDD') {
if (ssd1Group) ssd1Group.style.display = 'flex';
}
else if (type === '태블릿') {
if (modelGroup) modelGroup.style.display = 'flex';
if (ssd1Group) ssd1Group.style.display = 'flex';
}
}
function fillFormData(asset: HardwareAsset) {
setFieldValue('pc-asset-id', asset.id);
setFieldValue('pc-법인', asset.);
setFieldValue('pc-자산코드', asset.);
setFieldValue('pc-유형', asset.type);
setFieldValue('pc-사용자', asset.);
setFieldValue('pc-현사용조직', asset.);
setFieldValue('pc-이전사용조직', asset.);
setFieldValue('pc-상세용도', (asset as any).);
parseAndSetLocation(asset., 'pc-위치-빌딩', 'pc-위치-상세', 'pc-위치-기타-group', 'pc-위치-기타');
setFieldValue('pc-모델명', asset.);
setFieldValue('pc-OS', asset.OS);
setFieldValue('pc-CPU', asset.CPU);
setFieldValue('pc-RAM', asset.RAM);
setFieldValue('pc-SSD1', asset.SSD1);
setFieldValue('pc-SSD2', asset.SSD2);
setFieldValue('pc-구매일', asset.);
setFieldValue('pc-금액', asset.);
setFieldValue('pc-납품업체', asset.);
setFieldValue('pc-품의서명', asset.);
}
export function initPcModal(onSave: () => void, closeModalsCb: () => void) {
if (!document.getElementById('pc-asset-modal')) {
document.body.insertAdjacentHTML('beforeend', PC_MODAL_HTML);
}
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
const saveBtn = document.getElementById('btn-save-pc-asset');
const revertBtn = document.getElementById('btn-revert-pc-edit');
const deleteBtn = document.getElementById('btn-delete-pc-asset');
// 유형 및 상세용도 리스너
const typeSelect = document.getElementById('pc-유형') as HTMLSelectElement;
const detailPurposeSelect = document.getElementById('pc-상세용도') as HTMLSelectElement;
[typeSelect, detailPurposeSelect].forEach(el => {
el?.addEventListener('change', () => applyPcTypeSpecificUI());
});
bindLocationEvents('pc-위치-빌딩', 'pc-위치-상세', 'pc-위치-기타-group', 'pc-위치-기타');
const handleClose = () => { closeModalsCb(); isEditMode = false; };
document.getElementById('btn-close-pc-modal')?.addEventListener('click', handleClose);
document.getElementById('btn-cancel-pc-modal')?.addEventListener('click', handleClose);
revertBtn?.addEventListener('click', () => {
isEditMode = false;
pcForm.classList.replace('is-edit-mode', 'is-view-mode');
if (saveBtn) saveBtn.textContent = '수정';
revertBtn.classList.add('hidden');
if (currentAsset) fillFormData(currentAsset);
});
saveBtn?.addEventListener('click', () => {
if (!currentAsset) return;
if (!isEditMode) {
isEditMode = true;
pcForm.classList.replace('is-view-mode', 'is-edit-mode');
saveBtn.textContent = '저장';
revertBtn?.classList.remove('hidden');
return;
}
const type = getFieldValue('pc-유형');
const detailPurpose = getFieldValue('pc-상세용도');
const updated: any = {
...currentAsset,
법인: getFieldValue('pc-법인'),
자산코드: getFieldValue('pc-자산코드'),
현사용조직: getFieldValue('pc-현사용조직'),
이전사용조직: getFieldValue('pc-이전사용조직'),
사용자: getFieldValue('pc-사용자'),
상세용도: detailPurpose,
위치: getCombinedLocation('pc-위치-빌딩', 'pc-위치-상세', 'pc-위치-기타'),
모델명: getFieldValue('pc-모델명'),
OS: getFieldValue('pc-OS'),
CPU: getFieldValue('pc-CPU'),
RAM: getFieldValue('pc-RAM'),
SSD1: getFieldValue('pc-SSD1'),
SSD2: getFieldValue('pc-SSD2'),
구매일: getFieldValue('pc-구매일'),
금액: getFieldValue('pc-금액'),
납품업체: getFieldValue('pc-납품업체'),
type: type || 'PC'
};
saveHardwareAsset(updated);
onSave();
isEditMode = false;
pcForm.classList.replace('is-edit-mode', 'is-view-mode');
saveBtn.textContent = '수정';
revertBtn?.classList.add('hidden');
});
deleteBtn?.addEventListener('click', () => {
if (!currentAsset) return;
if (confirm('삭제하시겠습니까?')) {
deleteHardwareAsset(currentAsset.id);
onSave();
handleClose();
}
});
}
function renderHistory(assetId: string) {
const historyList = document.getElementById('pc-history-list');
if (!historyList) return;
const logs = state.masterData.logs
.filter(l => l.assetId === assetId)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
if (logs.length === 0) {
historyList.innerHTML = '<div class="empty-history">이력이 없습니다.</div>';
return;
}
historyList.innerHTML = logs.map(log => `
<div class="history-item">
<div class="history-date">${log.date}</div>
<div class="history-user">수정자: ${log.user}</div>
<div class="history-details">${log.details.replace(/\n/g, '<br>')}</div>
</div>
`).join('');
}

View File

@@ -1,184 +1,120 @@
import { state } from '../../core/state';
import { SoftwareAsset } from '../../core/excelHandler';
import { openModal, closeModals } from './BaseModal';
import { closeModals } from './BaseModal';
import { openSwUserModal } from './SWUserModal';
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw } from 'lucide';
import { CORP_LIST } from './SharedData';
import { CORP_LIST, TYPE_PREFIX_MAP } from './SharedData';
import {
generateOptionsHTML,
setFieldValue,
getFieldValue,
setEditLock
setEditLock,
createModalFrameHTML,
autoFillForm,
autoExtractForm
} from './ModalUtils';
let currentSwAsset: SoftwareAsset | null = null;
let isEditMode = false;
const SW_MODAL_HTML = `
<div id="sw-asset-modal" class="modal-overlay hidden">
<div class="modal-content wide">
<div class="modal-header">
<h2 id="sw-modal-title">소프트웨어 상세 정보</h2>
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
</div>
<div class="modal-body">
<div class="modal-body-split">
<div class="modal-form-area">
<form id="sw-asset-form" class="grid-form">
<input type="hidden" id="sw-asset-id" />
<input type="hidden" id="sw-asset-type" />
const SW_FIELD_MAP: Record<string, string> = {
'법인': '법인',
'자산번호': '자산번호',
'제품명': '제품명',
'수량': '수량',
'금액': '금액',
'구매일': '구매일',
'납품업체': '납품업체',
'비고': '비고',
'플랫폼명': '플랫폼명',
'부서': '부서',
'계정명': '계정명',
'결제수단': '결제수단',
'연결카드번호': '연결카드번호',
'결제일': '결제일',
'당월청구액': '당월청구액',
'라이선스유형': '라이선스유형',
'만료일': '만료일',
'라이선스키': '라이선스키'
};
<!-- Group 1: 기본 정보 (Identity) -->
<div class="form-section-title">기본 정보 (Identity)</div>
<div class="form-group">
<label for="sw-법인">구매법인</label>
<select id="sw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
</div>
<div class="form-group sw-standard-field">
<label for="sw-자산번호">자산번호</label>
<input type="text" id="sw-자산번호" readonly placeholder="자동 생성" />
</div>
<div class="form-group full-width">
<label for="sw-제품명">제품명 / 서비스명</label>
<input type="text" id="sw-제품명" required />
</div>
<div class="form-group cloud-only">
<label for="sw-플랫폼명">플랫폼명</label>
<input type="text" id="sw-플랫폼명" placeholder="예: AWS, Cafe24" />
</div>
<div class="form-group cloud-only">
<label for="sw-부서">담당부서</label>
<input type="text" id="sw-부서" />
</div>
<!-- Group 2: 라이선스 및 계약 (License/Contract) -->
<div class="form-section-title">라이선스 및 계약 정보</div>
<div class="form-group sw-standard-field" id="sw-license-type-group">
<label for="sw-라이선스유형">라이선스 유형</label>
<input type="text" id="sw-라이선스유형" />
</div>
<div class="form-group sw-standard-field" id="sw-license-key-group">
<label for="sw-라이선스키">라이선스 키</label>
<input type="text" id="sw-라이선스키" />
</div>
<div class="form-group sw-standard-field">
<label for="sw-수량">보유 수량</label>
<input type="number" id="sw-수량" min="0" />
</div>
<div class="form-group sw-standard-field">
<label for="sw-금액">도입 금액</label>
<input type="text" id="sw-금액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
</div>
<!-- Group 3: 클라우드 전용 정보 (Cloud Specific) -->
<div class="form-group cloud-only">
<label for="sw-계정명">계정명 (이메일)</label>
<input type="text" id="sw-계정명" />
</div>
<div class="form-group cloud-only">
<label for="sw-결제수단">결제수단</label>
<select id="sw-결제수단">
<option value="">선택안함</option>
<option value="법인카드">법인카드</option>
<option value="인보이스">인보이스</option>
</select>
</div>
<div class="form-group cloud-only">
<label for="sw-연결카드번호">연결카드번호(뒷4자리)</label>
<input type="text" id="sw-연결카드번호" maxlength="4" />
</div>
<div class="form-group cloud-only">
<label for="sw-결제일">결제일 (기준일)</label>
<input type="number" id="sw-결제일" min="1" max="31" />
</div>
<div class="form-group cloud-only">
<label for="sw-당월청구액">당월 청구액(원)</label>
<input type="text" id="sw-당월청구액" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
</div>
<!-- Group 4: 관리 정보 (Management) -->
<div class="form-section-title">관리 및 비고</div>
<div class="form-group sw-standard-field">
<label for="sw-구매일">구매일</label>
<input type="text" id="sw-구매일" />
</div>
<div class="form-group sw-standard-field" id="sw-expiry-group">
<label for="sw-만료일">만료일 (구독)</label>
<input type="text" id="sw-만료일" />
</div>
<div class="form-group sw-standard-field">
<label for="sw-납품업체">납품업체</label>
<input type="text" id="sw-납품업체" />
</div>
<div class="form-group full-width">
<label for="sw-비고">비고</label>
<textarea id="sw-비고" rows="2"></textarea>
</div>
</form>
<div id="sw-user-section" class="user-management-section" style="margin-top: 2rem;">
<div class="section-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h3 style="font-size:1rem; font-weight:600;">사용자 할당 현황</h3>
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">
할당 관리 <i data-lucide="plus" style="width:14px; height:14px;"></i>
</button>
</div>
<div id="sw-assigned-users-summary" class="user-summary-grid"></div>
</div>
</div>
<div class="modal-history-area">
<div class="history-header" style="display:flex; justify-content:space-between; align-items:center;">
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 업데이트 내역</h3>
<button type="button" id="btn-add-sw-log" class="btn btn-outline btn-sm cloud-only">
내역 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
</button>
</div>
<div id="sw-history-list" class="history-timeline"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button id="btn-delete-sw-asset" class="btn btn-outline btn-danger">삭제</button>
<div class="footer-actions">
<button id="btn-revert-sw-edit" class="btn btn-outline hidden">수정 취소</button>
<button id="btn-cancel-sw-modal" class="btn btn-outline">닫기</button>
<button id="btn-save-sw-asset" class="btn btn-primary">수정</button>
</div>
</div>
const SW_FORM_HTML = `
<!-- Group 1: 기본 정보 -->
<div class="form-section-title">기본 정보 (Identity)</div>
<div class="form-group">
<label for="sw-법인">구매법인</label>
<select id="sw-법인" required>${generateOptionsHTML(CORP_LIST)}</select>
</div>
<div class="form-group sw-standard-field">
<label for="sw-자산번호">자산번호</label>
<div class="input-with-btn">
<input type="text" id="sw-자산번호" readonly class="is-readonly-field" placeholder="번호 생성을 클릭하세요" />
<button type="button" id="btn-generate-sw-code" class="btn btn-outline btn-sm">생성</button>
</div>
</div>
<div class="form-group full-width">
<label for="sw-제품명">제품명 / 서비스명</label>
<input type="text" id="sw-제품명" required />
</div>
<div class="form-group cloud-only"><label for="sw-플랫폼명">플랫폼명</label><input type="text" id="sw-플랫폼명" placeholder="예: AWS, Cafe24" /></div>
<div class="form-group cloud-only"><label for="sw-부서">담당부서</label><input type="text" id="sw-부서" /></div>
<!-- 클라우드 이력 추가를 위한 간이 모달 -->
<div id="sw-log-modal" class="modal-overlay hidden" style="z-index: 1100;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h2>업데이트 내역 추가</h2>
<button id="btn-close-sw-log" class="btn-icon"><i data-lucide="x"></i></button>
</div>
<div class="modal-body">
<div class="grid-form" style="grid-template-columns: 1fr;">
<div class="form-group">
<label>날짜</label>
<input type="date" id="new-log-date" />
</div>
<div class="form-group">
<label>상세 내용</label>
<textarea id="new-log-details" rows="3" placeholder="예: 결제 금액 변동, 담당자 변경 등"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<div></div>
<div class="footer-actions">
<button id="btn-cancel-sw-log" class="btn btn-outline">취소</button>
<button id="btn-confirm-sw-log" class="btn btn-primary">추가</button>
</div>
</div>
<!-- Group 2: 라이선스 및 계약 -->
<div class="form-section-title">라이선스 및 계약 정보</div>
<div class="form-group sw-standard-field" id="sw-license-type-group"><label for="sw-라이선스유형">라이선스 유형</label><input type="text" id="sw-라이선스유형" /></div>
<div class="form-group sw-standard-field" id="sw-license-key-group"><label for="sw-라이선스키">라이선스 키</label><input type="text" id="sw-라이선스키" /></div>
<div class="form-group sw-standard-field"><label for="sw-수량">보유 수량</label><input type="number" id="sw-수량" min="0" /></div>
<div class="form-group sw-standard-field"><label for="sw-금액">도입 금액</label><input type="text" id="sw-금액" oninput="this.value=this.value.replace(/[^0-9]/g,'').replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g,',')" /></div>
<div class="form-group cloud-only"><label for="sw-계정명">계정명 (이메일)</label><input type="text" id="sw-계정명" /></div>
<div class="form-group cloud-only"><label for="sw-결제수단">결제수단</label><select id="sw-결제수단"><option value="">선택안함</option><option value="법인카드">법인카드</option><option value="인보이스">인보이스</option></select></div>
<div class="form-group cloud-only"><label for="sw-연결카드번호">연결카드번호(뒷4자리)</label><input type="text" id="sw-연결카드번호" maxlength="4" /></div>
<div class="form-group cloud-only"><label for="sw-결제일">결제일 (기준일)</label><input type="number" id="sw-결제일" min="1" max="31" /></div>
<div class="form-group cloud-only"><label for="sw-당월청구액">당월 청구액(원)</label><input type="text" id="sw-당월청구액" oninput="this.value=this.value.replace(/[^0-9]/g,'').replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g,',')" /></div>
<!-- Group 4: 관리 정보 -->
<div class="form-section-title">관리 및 비고</div>
<div class="form-group sw-standard-field"><label for="sw-구매일">구매연월</label><input type="text" id="sw-구매일" placeholder="YYYYMM" maxlength="6" /></div>
<div class="form-group sw-standard-field" id="sw-expiry-group"><label for="sw-만료일">만료일 (구독)</label><input type="text" id="sw-만료일" /></div>
<div class="form-group sw-standard-field"><label for="sw-납품업체">납품업체</label><input type="text" id="sw-납품업체" /></div>
<div class="form-group full-width"><label for="sw-비고">비고</label><textarea id="sw-비고" rows="2"></textarea></div>
<div id="sw-user-section" class="user-management-section" style="margin-top: 2rem;">
<div class="section-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h3 style="font-size:1rem; font-weight:600;">사용자 할당 현황</h3>
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">할당 관리 <i data-lucide="plus" style="width:14px; height:14px;"></i></button>
</div>
<div id="sw-assigned-users-summary" class="user-summary-grid"></div>
</div>
`;
function renderSwHistory(swId: string) {
const container = document.getElementById('sw-history-list');
if (!container) return;
const logs = (state.masterData.logs || []).filter(l => l.assetId === swId).sort((a,b) => new Date(b.date).getTime() - new Date(a.date).getTime());
if (logs.length === 0) { container.innerHTML = '<div class="empty-history">수정 이력이 없습니다.</div>'; return; }
container.innerHTML = logs.map(l => `
<div class="history-item">
<div class="history-date">${l.date}</div>
<div class="history-user">${l.user}</div>
<div class="history-details">${l.details.replace(/\n/g, '<br>')}</div>
</div>
`).join('');
}
function renderUserSummary(swId: string) {
const container = document.getElementById('sw-assigned-users-summary');
if (!container) return;
const userMapping = state.masterData.swUsers.find(u => u.sw_id === swId);
if (!userMapping || !userMapping.userData || userMapping.userData.length === 0) {
container.innerHTML = '<div class="empty-summary">할당된 사용자가 없습니다.</div>';
return;
}
container.innerHTML = userMapping.userData.map(u => `
<div class="user-badge-item"><span class="u-name">${u[3] || '이름없음'}</span><span class="u-dept">${u[1] || '부서없음'}</span></div>
`).join('');
}
function applySwTypeUI(type: string) {
const cloudFields = document.querySelectorAll('.cloud-only');
const swFields = document.querySelectorAll('.sw-standard-field');
@@ -195,7 +131,6 @@ function applySwTypeUI(type: string) {
cloudFields.forEach(el => (el as HTMLElement).style.display = 'none');
swFields.forEach(el => (el as HTMLElement).style.display = 'flex');
if (userSection) userSection.style.display = 'block';
if (type === '구독SW') {
if (keyGroup) keyGroup.style.display = 'none';
if (typeGroup) typeGroup.style.display = 'flex';
@@ -208,92 +143,41 @@ function applySwTypeUI(type: string) {
}
}
function fillSwFormData(asset: SoftwareAsset) {
setFieldValue('sw-asset-id', asset.id);
setFieldValue('sw-asset-type', asset.type);
setFieldValue('sw-법인', asset.);
setFieldValue('sw-자산번호', asset. || '');
setFieldValue('sw-제품명', asset.);
setFieldValue('sw-수량', asset.);
setFieldValue('sw-금액', asset.);
setFieldValue('sw-구매일', asset. || '');
setFieldValue('sw-납품업체', asset. || '');
setFieldValue('sw-비고', asset. || '');
if (asset.type === '클라우드') {
setFieldValue('sw-플랫폼명', (asset as any). || '');
setFieldValue('sw-부서', (asset as any). || '');
setFieldValue('sw-계정명', (asset as any). || '');
setFieldValue('sw-결제수단', (asset as any). || '');
setFieldValue('sw-연결카드번호', (asset as any). || '');
setFieldValue('sw-결제일', (asset as any). || '');
setFieldValue('sw-당월청구액', (asset as any). || '');
} else if (asset.type === '구독SW') {
setFieldValue('sw-라이선스유형', (asset as any). || '');
setFieldValue('sw-만료일', (asset as any). || '');
} else {
setFieldValue('sw-라이선스키', (asset as any). || '');
}
renderUserSummary(asset.id);
renderSwHistory(asset.id);
}
function renderUserSummary(swId: string) {
const container = document.getElementById('sw-assigned-users-summary');
if (!container) return;
const userMapping = state.masterData.swUsers.find(u => u.sw_id === swId);
if (!userMapping || !userMapping.userData || userMapping.userData.length === 0) {
container.innerHTML = '<div class="empty-summary">할당된 사용자가 없습니다.</div>';
return;
}
container.innerHTML = userMapping.userData.map(u => `
<div class="user-badge-item">
<span class="u-name">${u[3] || '이름없음'}</span>
<span class="u-dept">${u[1] || '부서없음'}</span>
</div>
`).join('');
}
function renderSwHistory(swId: string) {
const container = document.getElementById('sw-history-list');
if (!container) return;
const logs = (state.masterData.logs || []).filter(l => l.assetId === swId);
if (logs.length === 0) {
container.innerHTML = '<div class="empty-history">수정 이력이 없습니다.</div>';
return;
}
container.innerHTML = logs.map(l => `
<div class="history-item">
<div class="history-date">${l.date}</div>
<div class="history-user">${l.user}</div>
<div class="history-details">${l.details}</div>
</div>
`).join('');
}
export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' = 'view') {
currentSwAsset = asset;
const modal = document.getElementById('sw-asset-modal')!;
// 수정 잠금 상태 제어
setEditLock('sw-asset-form', mode, {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit'
revertBtnId: 'btn-revert-sw-edit',
generateBtnId: 'btn-generate-sw-code',
addLogBtnId: 'btn-add-sw-log'
});
isEditMode = (mode === 'add');
fillSwFormData(asset);
autoFillForm('sw', asset, SW_FIELD_MAP);
applySwTypeUI(asset.type);
renderUserSummary(asset.id);
renderSwHistory(asset.id);
modal.classList.remove('hidden');
createIcons({ icons: { X, History, Plus } });
}
export function initSwModal(onSave: () => void, closeModals: () => void) {
export function initSwModal(onSave: () => void, closeModalsCb: () => void) {
if (!document.getElementById('sw-asset-modal')) {
document.body.insertAdjacentHTML('beforeend', SW_MODAL_HTML);
const html = createModalFrameHTML('sw', '소프트웨어 상세 정보', SW_FORM_HTML, {
historyTitle: '업데이트 내역',
addLogBtnId: 'btn-add-sw-log'
});
document.body.insertAdjacentHTML('beforeend', html);
const logModalHTML = `
<div id="sw-log-modal" class="modal-overlay hidden" style="z-index: 1100;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header"><h2>업데이트 내역 추가</h2><button id="btn-close-sw-log" class="btn-icon"><i data-lucide="x"></i></button></div>
<div class="modal-body"><div class="grid-form" style="grid-template-columns: 1fr;"><div class="form-group"><label>날짜</label><input type="date" id="new-log-date" /></div><div class="form-group"><label>상세 내용</label><textarea id="new-log-details" rows="3"></textarea></div></div></div>
<div class="modal-footer"><div></div><div class="footer-actions"><button id="btn-cancel-sw-log" class="btn btn-outline">취소</button><button id="btn-confirm-sw-log" class="btn btn-primary">추가</button></div></div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', logModalHTML);
}
const form = document.getElementById('sw-asset-form') as HTMLFormElement;
@@ -302,123 +186,100 @@ export function initSwModal(onSave: () => void, closeModals: () => void) {
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
const userUpdateBtn = document.getElementById('btn-open-sw-update')!;
const logAddBtn = document.getElementById('btn-add-sw-log')!;
const logModal = document.getElementById('sw-log-modal')!;
const closeModalAction = () => { closeModals(); isEditMode = false; };
const closeModalAction = () => { closeModalsCb(); isEditMode = false; };
document.getElementById('btn-close-sw-modal')?.addEventListener('click', closeModalAction);
document.getElementById('btn-cancel-sw-modal')?.addEventListener('click', closeModalAction);
revertBtn.addEventListener('click', () => {
setEditLock('sw-asset-form', 'view', {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit'
setEditLock('sw-asset-form', 'view', {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit',
generateBtnId: 'btn-generate-sw-code',
addLogBtnId: 'btn-add-sw-log'
});
isEditMode = false;
if (currentSwAsset) fillSwFormData(currentSwAsset);
if (currentSwAsset) openSwModal(currentSwAsset, 'view');
});
document.getElementById('btn-generate-sw-code')?.addEventListener('click', async () => {
const typeValue = getFieldValue('sw-asset-type');
const purchaseDate = getFieldValue('sw-구매일');
const typeCode = TYPE_PREFIX_MAP[typeValue] || 'SW';
const dateStr = purchaseDate.replace(/[^0-9]/g, '');
if (dateStr.length < 6) { alert('올바른 구매연월(YYYYMM)을 입력해주세요.'); return; }
const prefix = `${typeCode}-${dateStr.substring(0, 6)}-`;
try {
const res = await fetch(`http://localhost:3000/api/generate-asset-code?prefix=${prefix}`);
const data = await res.json();
if (data.nextCode) setFieldValue('sw-자산번호', data.nextCode);
} catch (err) { alert('자산번호 생성에 실패했습니다.'); }
});
// YYYYMM 입력 제한 로직 (숫자 6자리)
document.getElementById('sw-구매일')?.addEventListener('input', (e) => {
const target = e.target as HTMLInputElement;
target.value = target.value.replace(/[^0-9]/g, '').substring(0, 6);
});
saveBtn.addEventListener('click', () => {
if (!currentSwAsset) return;
if (!isEditMode) {
setEditLock('sw-asset-form', 'edit', {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit'
setEditLock('sw-asset-form', 'edit', {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit',
generateBtnId: 'btn-generate-sw-code',
addLogBtnId: 'btn-add-sw-log'
});
isEditMode = true;
return;
}
const extracted = autoExtractForm('sw', SW_FIELD_MAP);
const updated = { ...currentSwAsset, ...extracted, 수량: parseInt(extracted. || '0') };
const type = getFieldValue('sw-asset-type');
const updated: any = {
...currentSwAsset,
법인: getFieldValue('sw-법인'),
자산번호: getFieldValue('sw-자산번호'),
제품명: getFieldValue('sw-제품명'),
수량: parseInt(getFieldValue('sw-수량') || '0'),
금액: getFieldValue('sw-금액'),
구매일: getFieldValue('sw-구매일'),
납품업체: getFieldValue('sw-납품업체'),
비고: getFieldValue('sw-비고'),
type: type
};
if (type === '클라우드') {
updated. = getFieldValue('sw-플랫폼명');
updated. = getFieldValue('sw-부서');
updated. = getFieldValue('sw-계정명');
updated. = getFieldValue('sw-결제수단');
updated. = getFieldValue('sw-연결카드번호');
updated. = getFieldValue('sw-결제일');
updated. = getFieldValue('sw-당월청구액');
} else if (type === '구독SW') {
updated. = getFieldValue('sw-라이선스유형');
updated. = getFieldValue('sw-만료일');
} else {
updated. = getFieldValue('sw-라이선스키');
}
// 데이터 저장 로직 (state 업데이트)
let targetList: SoftwareAsset[] = [];
if (type === '구독SW') targetList = state.masterData.subSw;
else if (type === '영구SW') targetList = state.masterData.permSw;
else if (type === '클라우드') targetList = state.masterData.cloud;
if (updated.type === '구독SW') targetList = state.masterData.subSw;
else if (updated.type === '영구SW') targetList = state.masterData.permSw;
else targetList = (state.masterData as any).cloud || [];
const idx = targetList.findIndex(a => a.id === updated.id);
if (idx > -1) targetList[idx] = updated;
else targetList.push(updated);
if (idx > -1) targetList[idx] = updated; else targetList.push(updated);
onSave();
setEditLock('sw-asset-form', 'view', {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit'
setEditLock('sw-asset-form', 'view', {
saveBtnId: 'btn-save-sw-asset',
revertBtnId: 'btn-revert-sw-edit',
generateBtnId: 'btn-generate-sw-code',
addLogBtnId: 'btn-add-sw-log'
});
isEditMode = false;
});
deleteBtn.addEventListener('click', () => {
if (!currentSwAsset) return;
if (confirm('삭제하시겠습니까?')) {
if (currentSwAsset && confirm('삭제하시겠습니까?')) {
const type = currentSwAsset.type;
if (type === '구독SW') state.masterData.subSw = state.masterData.subSw.filter(a => a.id !== currentSwAsset!.id);
else if (type === '영구SW') state.masterData.permSw = state.masterData.permSw.filter(a => a.id !== currentSwAsset!.id);
else if (type === '클라우드') state.masterData.cloud = state.masterData.cloud.filter(a => a.id !== currentSwAsset!.id);
onSave();
closeModalAction();
onSave(); closeModalAction();
}
});
userUpdateBtn.addEventListener('click', () => {
if (currentSwAsset) openSwUserModal(currentSwAsset);
});
// 이력 추가 모달 로직
const logModal = document.getElementById('sw-log-modal')!;
userUpdateBtn.addEventListener('click', () => { if (currentSwAsset) openSwUserModal(currentSwAsset); });
logAddBtn.addEventListener('click', () => {
logModal.classList.remove('hidden');
(document.getElementById('new-log-date') as HTMLInputElement).value = new Date().toISOString().split('T')[0];
(document.getElementById('new-log-details') as HTMLTextAreaElement).value = '';
});
document.getElementById('btn-close-sw-log')?.addEventListener('click', () => logModal.classList.add('hidden'));
document.getElementById('btn-cancel-sw-log')?.addEventListener('click', () => logModal.classList.add('hidden'));
document.getElementById('btn-confirm-sw-log')?.addEventListener('click', () => {
if (!currentSwAsset) return;
const date = (document.getElementById('new-log-date') as HTMLInputElement).value;
const details = (document.getElementById('new-log-details') as HTMLTextAreaElement).value;
if (!date || !details) { alert('날짜와 내용을 입력해주세요.'); return; }
if (!date || !details) return;
state.masterData.logs = state.masterData.logs || [];
state.masterData.logs.push({
id: Math.random().toString(36).substring(2, 9),
assetId: currentSwAsset.id,
date,
user: '관리자',
details
});
logModal.classList.add('hidden');
renderSwHistory(currentSwAsset.id);
state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: currentSwAsset.id, date, user: '관리자', details });
logModal.classList.add('hidden'); renderSwHistory(currentSwAsset.id);
});
}

View File

@@ -29,5 +29,6 @@ export const TYPE_PREFIX_MAP: Record<string, string> = {
'서버': 'SVR', 'PC': 'PC', 'NAS': 'NAS', 'DAS': 'DAS', '스토리지': 'STO',
'CPU': 'CPU', 'HDD': 'HDD', 'RAM': 'RAM', 'GPU': 'GPU',
'모바일': 'MOB', '노트북': 'PC', '태블릿': 'TAB',
'개인PC': 'PC', '모바일기기': 'MOB'
'개인PC': 'PC', '모바일기기': 'MOB',
'구독SW': 'SSW', '영구SW': 'PSW'
};

View File

@@ -1,6 +1,43 @@
import * as XLSX from 'xlsx';
import { HardwareAsset, SoftwareAsset, SWUser, HardwareLog, MasterAssetData } from './state';
const PC_HEADERS = ['법인', '자산코드', '사용자', '위치', 'CPU', 'GPU', 'RAM', 'SSD1', 'SSD2', 'HDD1', 'HDD2', 'IP주소', 'HW사양', '구매일', '금액', '납품업체', '품의서명', '비고'];
const SERVER_HEADERS = ['구매법인', '자산번호', '구매일자', '유형', '용도', '상세내용', '현사용조직', '이전사용조직', '설치위치', '담당자(정)', '담당자(부)', 'IP 주소 1', 'IP 주소 2', '원격도구', '서버 ID', '서버 PW', '모델명', 'OS', 'CPU', 'RAM', 'GPU', 'Storage 1', 'Storage 2', 'Storage 3', '모니터링', '비고'];
const STORAGE_HEADERS = ['구매법인', '유형', '자산코드', '명칭', '위치', '모델명', '용량', '담당자(정)', '담당자(부)', 'IP주소', 'MAC주소', '구매일', '금액', '납품업체', '품의서명', '비고'];
const EQUIP_HEADERS = ['구매법인', '비품유형', '자산코드', '명칭', '위치', '관리자', 'IP주소', 'MACaddress', 'HW사양', 'OS', '구매일', '금액', '납품업체', '품의서명', '비고'];
const MOBILE_HEADERS = ['구매법인', '자산코드', '명칭', '위치', '관리자', '기기유형', 'OS', '구매일', '금액', '납품업체', '품의서명', '비고'];
const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '만료일', '라이선스유형', '금액', '수량', '계정명', '납품업체', '비고'];
const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '라이선스키', '금액', '수량', '계정명', '납품업체', '비고'];
const CLOUD_HEADERS = ['ID', '플랫폼명', '법인', '부서', '사용용도(제품명)', '계정명', '결제수단', '결제일', '연결카드번호', '당월청구액', '비고'];
export function downloadTemplate() {
const wb = XLSX.utils.book_new();
const tabConfigs = [
{ name: '개인PC', headers: PC_HEADERS },
{ name: '서버', headers: SERVER_HEADERS },
{ name: '스토리지', headers: STORAGE_HEADERS },
{ name: '전산비품', headers: EQUIP_HEADERS },
{ name: '모바일기기', headers: MOBILE_HEADERS }
];
tabConfigs.forEach(config => {
const ws = XLSX.utils.aoa_to_sheet([config.headers]);
ws['!cols'] = Array(config.headers.length).fill({ wch: 18 });
XLSX.utils.book_append_sheet(wb, ws, config.name);
});
const SW_TABS = ['구독SW', '영구SW', '클라우드'];
SW_TABS.forEach(tab => {
let hd = tab === '구독SW' ? SUB_SW_HEADERS : (tab === '클라우드' ? CLOUD_HEADERS : PERM_SW_HEADERS);
const ws = XLSX.utils.aoa_to_sheet([hd]);
ws['!cols'] = Array(hd.length).fill({ wch: 18 });
XLSX.utils.book_append_sheet(wb, ws, tab);
});
XLSX.writeFile(wb, 'itam_assets_template_full.xlsx');
}
export async function parseExcel(file: File): Promise<MasterAssetData> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
@@ -26,122 +63,137 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
rows.forEach(r => data.pc.push({
id: Math.random().toString(36).substring(2, 9),
type: '개인PC',
자산번호: r['자산번호'] || '',
관리번호: r['관리번호'] || r['관리코드'] || '',
사용조직: r['사용조직'] || '',
모델명: r['모델명'] || '',
OS: r['OS'] || '',
법인: r['법인'] || '',
자산코드: r['자산코드'] || '',
사용: r['사용'] || '',
위치: r['위치'] || '',
CPU: r['CPU'] || '',
GPU: r['GPU'] || '',
RAM: r['RAM'] || '',
SSD1: r['SSD1'] || '',
SSD2: r['SSD2'] || '',
HDD1: r['HDD1'] || '',
IP주소: r['IP주소'] || '',
HW사양: r['HW사양'] || '',
도입: r['도입일'] || '',
구매가: r['구매가'] || r['금액'] || '',
비고: r['비고'] || ''
구매: r['구매일'] || '',
금액: r['금액'] || '',
납품업체: r['납품업체'] || '',
품의서명: r['품의서명'] || '',
비고: r['비고'] || '',
MACaddress: '', OS: '', : ''
}));
} else if (sheetName === '서버') {
rows.forEach(r => data.server.push({
id: Math.random().toString(36).substring(2, 9),
type: '서버',
자산번호: r['자산번호'] || '',
관리번호: r['관리번호'] || r['관리코드'] || '',
서버용: r['서버용도'] || r['용도'] || '',
상태: r['상태'] || r['운영단계'] || '',
실사용조직: r['실사용조직'] || r['사용조직'] || '',
관리조직: r['관리조직'] || '',
설치장소: r['설치장소'] || r['위치'] || '',
세부위치: r['세부위치'] || '',
IP주소: r['IP주소'] || r['IP 주소 1'] || '',
IP2: r['IP2'] || r['IP 주소 2'] || '',
원격방법: r['원격방법'] || r['원격접속'] || '',
서버ID: r['서버ID'] || '',
서버PW: r['서버PW'] || '',
법인: r['구매법인'] || r['법인'] || '',
자산코드: r['자산번호'] || r['자산코드'] || '',
입일: r['구매일자'] || r['구매일'] || '',
서버용도: r['용도'] || r['서버용도'] || '',
상세: r['상세내용'] || '',
실사용조직: r['현사용조직'] || r['실사용조직'] || '',
이전사용조직: r['이전사용조직'] || '',
위치: r['설치위치'] || r['위치'] || '',
관리조직: r['담당자(정)'] || r['관리조직'] || '',
IP주소: r['IP 주소 1'] || r['IP주소'] || '',
IP2: r['IP 주소 2'] || '',
원격방법: r['원격도구'] || r['원격방법'] || '',
서버ID: r['서버 ID'] || r['서버ID'] || '',
서버PW: r['서버 PW'] || r['서버PW'] || '',
모델명: r['모델명'] || '',
OS: r['OS'] || '',
CPU: r['CPU'] || '',
RAM: r['RAM'] || '',
SSD1: r['SSD1'] || r['Storage 1'] || '',
SSD2: r['SSD2'] || r['Storage 2'] || '',
GPU: r['GPU'] || '',
SSD1: r['Storage 1'] || r['SSD1'] || '',
SSD2: r['Storage 2'] || r['SSD2'] || '',
모니터링: r['모니터링'] || '',
비고: r['비고'] || ''
비고: r['비고'] || '',
: '서버'
}));
} else if (sheetName === '스토리지') {
rows.forEach(r => data.storage.push({
id: Math.random().toString(36).substring(2, 9),
type: '스토리지',
자산번호: r['자산번호'] || '',
관리번호: r['관리번호'] || '',
법인: r['구매법인'] || r['법인'] || '',
자산코드: r['자산코드'] || '',
명칭: r['명칭'] || '',
위치: r['위치'] || '',
모델명: r['모델명'] || '',
용량: r['용량'] || '',
IP주소: r['IP주소'] || '',
도입일: r['도입일'] || '',
구매: r['구매가'] || r['금액'] || '',
비고: r['비고'] || ''
MACaddress: r['MAC주소'] || '',
구매: r['구매'] || '',
금액: r['금액'] || '',
납품업체: r['납품업체'] || '',
품의서명: r['품의서명'] || '',
비고: r['비고'] || '',
: '스토리지'
}));
} else if (sheetName === '기타자산') {
} else if (sheetName === '기기' || sheetName === '전산비품') {
rows.forEach(r => data.equip.push({
id: Math.random().toString(36).substring(2, 9),
type: '기타자산',
자산구분: r['자산구분'] || r['구분'] || '',
자산번호: r['자산번호'] || '',
관리번호: r['관리번호'] || '',
모델: r['모델명'] || '',
type: '전산비품',
법인: r['구매법인'] || r['법인'] || '',
자산번호: r['자산코드'] || '',
자산구분: r['비품유형'] || r['유형'] || '',
: r['명'] || '',
위치: r['위치'] || '',
사용자: r['관리자'] || '',
IP주소: r['IP주소'] || '',
HW사양: r['HW사양'] || '',
OS: r['OS'] || '',
도입일: r['도입일'] || '',
구매가: r['구매가'] || r['금액'] || '',
도입일: r['구매일'] || '',
구매가: r['금액'] || '',
비고: r['비고'] || ''
}));
} else if (sheetName === '모바일') {
} else if (sheetName === '모바일' || sheetName === '모바일기기') {
rows.forEach(r => data.mobile.push({
id: Math.random().toString(36).substring(2, 9),
type: '모바일',
자산번호: r['자산번호'] || '',
관리번호: r['관리번호'] || '',
모델: r['모델명'] || '',
사용자: r['사용자'] || '',
도입일: r['도입일'] || '',
구매가: r['구매가'] || r['금액'] || '',
법인: r['구매법인'] || r['법인'] || '',
자산번호: r['자산코드'] || '',
: r['명'] || '',
위치: r['위치'] || '',
사용자: r['관리자'] || '',
OS: r['OS'] || '',
도입일: r['구매일'] || '',
구매가: r['금액'] || '',
비고: r['비고'] || ''
}));
} else if (sheetName === '구독SW') {
rows.forEach(r => data.subSw.push({
id: r['ID'] || Math.random().toString(36).substring(2, 9),
type: '구독SW',
분야: r['분야'] || '',
법인: r['법인'] || '',
부서: r['부서'] || '',
자산번호: r['자산번호'] || '',
소프트웨어명: r['소프트웨어명'] || '',
제품명: r['제품명'] || '',
구매일: r['구매일'] || '',
만료일: r['만료일'] || '',
라이선스유형: r['라이선스유형'] || '',
금액: r['금액'] || '',
수량: parseInt(r['수량'] || '1', 10),
계정명: r['계정명'] || '',
납품업체: r['납품업체'] || '',
비고: r['비고'] || ''
}));
} else if (sheetName === '영구SW') {
rows.forEach(r => data.permSw.push({
id: r['ID'] || Math.random().toString(36).substring(2, 9),
type: '영구SW',
분야: r['분야'] || '',
법인: r['법인'] || '',
부서: r['부서'] || '',
자산번호: r['자산번호'] || '',
소프트웨어명: r['소프트웨어명'] || '',
제품명: r['제품명'] || '',
구매일: r['구매일'] || '',
라이선스키: r['라이선스키'] || '',
금액: r['금액'] || '',
수량: parseInt(r['수량'] || '1', 10),
계정명: r['계정명'] || '',
납품업체: r['납품업체'] || '',
비고: r['비고'] || ''
}));
} else if (sheetName === 'SW_사용현황') {
rows.forEach(r => data.swUsers.push({
id: r['id'] || Math.random().toString(36).substring(2, 9),
swId: r['swId'] || '',
부서: r['부서'] || '',
이름: r['이름'] || '',
직급: r['직급'] || '',
사번: r['사번'] || '',
사용기간: r['사용기간'] || ''
}));
} else if (sheetName === 'History') {
rows.forEach(r => data.logs.push({
id: r['id'] || Math.random().toString(36).substring(2, 9),
@@ -161,23 +213,21 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
});
}
export function exportToExcel(data: any, fileName: string) {
export function exportToExcel(masterData: MasterAssetData) {
const wb = XLSX.utils.book_new();
// HW Tabs
if (data.pc) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.pc), '개인PC');
if (data.server) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.server), '서버');
if (data.storage) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.storage), '스토리지');
if (data.equip) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.equip), '기타자산');
if (data.mobile) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.mobile), '모바일');
// SW Tabs
if (data.subSw) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.subSw), '구독SW');
if (data.permSw) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.permSw), '영구SW');
if (data.swUsers) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.swUsers), 'SW_사용현황');
// Logs
if (data.logs) XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data.logs), 'History');
const exportMap = [
{ tab: '개인PC', list: masterData.pc, headers: PC_HEADERS, map: (a: any) => [a., a., a., a., a.CPU, a.GPU, a.RAM, a.SSD1, a.SSD2, a.HDD1, a.HDD2, a.IP주소, a.HW사양, a., a., a., a., a.] },
{ tab: '서버', list: masterData.server, headers: SERVER_HEADERS, map: (a: any) => [a., a., a., '물리', a., a., a., a., a., a., '', a.IP주소, a.IP2, a., a.ID, a.PW, a., a.OS, a.CPU, a.RAM, a.GPU, a.SSD1, a.SSD2, '', a., a.] },
{ tab: '스토리지', list: masterData.storage, headers: STORAGE_HEADERS, map: (a: any) => [a., '스토리지', a., a., a., a., a., '', '', a.IP주소, a.MACaddress, a., a., a., a., a.] },
{ tab: '전산비품', list: masterData.equip, headers: EQUIP_HEADERS, map: (a: any) => [a., a., a., a., a., a., a.IP주소, '', a.HW사양, a.OS, a., a., '', '', a.] },
{ tab: '모바일기기', list: masterData.mobile, headers: MOBILE_HEADERS, map: (a: any) => [a., a., a., a., a., '모바일', a.OS, a., a., '', '', a.] },
{ tab: '구독SW', list: masterData.subSw, headers: SUB_SW_HEADERS, map: (a: any) => [a.id, a., a., a., a., a., a., a., a., a., a., a., a.] },
{ tab: '영구SW', list: masterData.permSw, headers: PERM_SW_HEADERS, map: (a: any) => [a.id, a., a., a., a., a., a., a., a., a., a., a.] }
];
XLSX.writeFile(wb, fileName);
exportMap.forEach(m => {
const ws = XLSX.utils.aoa_to_sheet([m.headers, ...m.list.map(m.map)]);
XLSX.utils.book_append_sheet(wb, ws, m.tab);
});
XLSX.writeFile(wb, `itam_master_full_${new Date().toISOString().split('T')[0]}.xlsx`);
}

View File

@@ -4,7 +4,6 @@ import { renderDashboard } from './views/DashboardView';
import { renderSWTable } from './views/SW_Table';
import { downloadTemplate, exportToExcel, parseExcel, HardwareAsset, SoftwareAsset, SWUser } from './core/excelHandler';
import { initBaseModal } from './components/Modal/BaseModal';
import { initPcModal } from './components/Modal/PCModal';
import { initHwModal, openHwModal } from './components/Modal/HWModal';
import { initSwModal, openSwModal } from './components/Modal/SWModal';
import { initSwUserModal } from './components/Modal/SWUserModal';
@@ -75,7 +74,6 @@ function initApp() {
});
// 모달 초기화
initPcModal(() => { saveAllHardwareToDB(); renderSWTable(mainContent); }, closeAllModals);
initHwModal(() => { saveAllHardwareToDB(); renderSWTable(mainContent); }, closeAllModals);
initSwModal(() => {
@@ -125,8 +123,14 @@ function initApp() {
const cat = state.activeCategory;
if (cat === 'hw') {
// 하드웨어 대시보드 또는 개별 탭에서 추가
const defaultType = (tab === '대시보드') ? '' : tab;
// 탭 명칭을 실제 유형명으로 매핑
let defaultType = '';
if (tab === '개인PC') defaultType = 'PC';
else if (tab === '서버') defaultType = '서버';
else if (tab === '스토리지') defaultType = '스토리지';
else if (tab === '전산비품') defaultType = 'CPU';
else if (tab === '모바일기기') defaultType = '모바일';
openHwModal({
id: Math.random().toString(36).substring(2, 9),
type: defaultType,

View File

@@ -180,6 +180,10 @@
background-color: var(--white);
}
.form-group textarea {
resize: none;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
@@ -226,6 +230,9 @@
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
@@ -238,6 +245,35 @@
color: var(--text-main);
}
/* 읽기 전용 필드 (자산번호 등) 통일 스타일 */
.is-readonly-field {
border-color: transparent !important;
background-color: transparent !important;
pointer-events: none !important;
color: var(--text-main) !important;
font-weight: 600 !important;
cursor: default;
padding-left: 0 !important;
}
/* 입력 필드 + 버튼 그룹 (자산번호 생성 등) */
.input-with-btn {
display: flex;
gap: 0.5rem;
align-items: center;
width: 100%;
}
.input-with-btn input {
flex: 1;
min-width: 0; /* flex 컨테이너 안에서 너비 압축 방지 */
}
.input-with-btn .btn {
flex-shrink: 0;
white-space: nowrap;
}
.history-timeline {
flex: 1;
overflow-y: auto;

View File

@@ -105,7 +105,7 @@ export function renderHwDashboard(container: HTMLElement) {
<th>유형</th>
<th>모델명</th>
<th>사용자/담당자</th>
<th>구매</th>
<th>구매연월</th>
<th>연령</th>
</tr>
</thead>

View File

@@ -22,6 +22,9 @@ export function renderEquipmentList(container: HTMLElement) {
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> 필터 초기화
</button>
<button id="btn-add-equip" class="btn btn-primary" style="margin-left: auto;">
<i data-lucide="plus"></i> 자산 추가
</button>
`;
container.appendChild(filterBar);
@@ -39,7 +42,7 @@ export function renderEquipmentList(container: HTMLElement) {
<th style="text-align:center;">모델명</th>
<th style="text-align:center;">보관위치</th>
<th style="text-align:center;">관리자</th>
<th style="text-align:center;">구매</th>
<th style="text-align:center;">구매연월</th>
<th style="text-align:center;">금액</th>
</tr>
</thead>

View File

@@ -22,6 +22,9 @@ export function renderMobileList(container: HTMLElement) {
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
<i data-lucide="refresh-ccw"></i> 필터 초기화
</button>
<button id="btn-add-mobile" class="btn btn-primary" style="margin-left: auto;">
<i data-lucide="plus"></i> 자산 추가
</button>
`;
container.appendChild(filterBar);
@@ -38,7 +41,7 @@ export function renderMobileList(container: HTMLElement) {
<th style="text-align:center;">명칭</th>
<th style="text-align:center;">보관위치</th>
<th style="text-align:center;">관리자</th>
<th style="text-align:center;">구매</th>
<th style="text-align:center;">구매연월</th>
<th style="text-align:center;">금액</th>
</tr>
</thead>
@@ -106,7 +109,8 @@ export function renderMobileList(container: HTMLElement) {
(document.getElementById('filter-keyword') as HTMLInputElement).value = '';
(document.getElementById('filter-corp') as HTMLSelectElement).value = '';
updateTable();
});
});
updateTable();
}
updateTable();
}

View File

@@ -1,5 +1,5 @@
import { state } from '../../core/state';
import { openPcModal } from '../../components/Modal/PCModal';
import { openHwModal } from '../../components/Modal/HWModal';
import { formatInline, sortAssets } from '../../core/utils';
import { createIcons, Paperclip, RefreshCcw } from 'lucide';
@@ -28,7 +28,7 @@ export function renderPcList(container: HTMLElement) {
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-container';
const table = document.createElement('table');
table.innerHTML = `<thead><tr><th>No</th><th>구매법인</th><th>현 사용조직</th><th>자산코드</th><th>사용자</th><th>위치</th><th>CPU</th><th>RAM</th><th>Storage</th><th>구매</th><th>금액</th><th>품의서</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
table.innerHTML = `<thead><tr><th>No</th><th>구매법인</th><th>현 사용조직</th><th>자산코드</th><th>사용자</th><th>위치</th><th>CPU</th><th>RAM</th><th>Storage</th><th>구매연월</th><th>금액</th><th>품의서</th><th>관리</th></tr></thead><tbody id="dynamic-tbody"></tbody>`;
tableWrapper.appendChild(table);
container.appendChild(tableWrapper);
@@ -69,12 +69,12 @@ export function renderPcList(container: HTMLElement) {
<td>${asset.CPU||''}</td>
<td>${asset.RAM||''}</td>
<td>${formatInline(storage)}</td>
<td>${asset.||''}</td>
<td>${asset. || 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">수정</button></td>
`;
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openPcModal(asset, 'view'); });
tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); });
tbody.appendChild(tr);
});
createIcons({ icons: { Paperclip } });

View File

@@ -102,7 +102,8 @@ export function renderServerList(container: HTMLElement) {
(document.getElementById('filter-corp') as HTMLSelectElement).value = '';
(document.getElementById('filter-org-unit') as HTMLSelectElement).value = '';
updateTable();
});
});
updateTable();
}
updateTable();
}

View File

@@ -49,7 +49,7 @@ export function renderSwList(container: HTMLElement) {
<th style="text-align:center;">구매법인</th>
<th style="text-align:center;">부서</th>
<th style="text-align:center;">제품명</th>
<th style="text-align:center;">구매</th>
<th style="text-align:center;">구매연월</th>
${isSub ? '<th style="text-align:center;">구독일</th>' : ''}
<th style="text-align:center;">금액</th>
<th style="text-align:center;">수량</th>