chore: clean up build artifacts, temporary excel locks, duplicate plans, and commit current project state
This commit is contained in:
@@ -1,280 +1,280 @@
|
||||
import { createIcons, BookOpen, X, ChevronDown, ChevronRight, RefreshCw } from 'lucide';
|
||||
import { state } from '../core/state';
|
||||
import './guide.css';
|
||||
|
||||
// ─── 자산별 가이드 콘텐츠 정의 (SW_Table 브랜치 전체 복구) ───
|
||||
interface GuideTabConfig {
|
||||
id: string;
|
||||
label: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const GUIDE_TABS: GuideTabConfig[] = [
|
||||
{
|
||||
id: 'overview',
|
||||
label: '📋 개요',
|
||||
content: `
|
||||
<section class="guide-section">
|
||||
<h3>IT 자산관리 시스템 개요</h3>
|
||||
<p class="guide-text">
|
||||
HM IT 자산관리 시스템(ITAM)은 기업의 IT 자산을 <strong>도입부터 폐기까지</strong> 전 과정에서 효율적으로 관리하기 위한 통합 플랫폼입니다.<br>
|
||||
하드웨어(PC, 서버, 스토리지, 전산비품, 모바일기기)와 소프트웨어(구독SW, 영구SW, 클라우드)를 체계적으로 추적하고 유지보수합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>전체 자산관리 프로세스</h3>
|
||||
<div class="flow-container">
|
||||
<div class="flow-row">
|
||||
<div class="flow-step">
|
||||
<span class="step-number">1</span>
|
||||
<div><span class="step-label">도입/구매</span><p class="step-desc">자산 구매 요청 → 승인 → 발주</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">2</span>
|
||||
<div><span class="step-label">등록/배정</span><p class="step-desc">자산번호 부여 → 시스템 등록 → 사용자 할당</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">3</span>
|
||||
<div><span class="step-label">운영/유지</span><p class="step-desc">현황 모니터링 → 점검/수리 → 이력 관리</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">4</span>
|
||||
<div><span class="step-label">반납/폐기</span><p class="step-desc">자산 회수 → 데이터 소거 → 폐기 처리</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>시스템 기본 사용방법</h3>
|
||||
<table class="guide-info-table">
|
||||
<thead><tr><th>기능</th><th>방법</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>자산 조회</strong></td><td>상단 카테고리(하드웨어/소프트웨어) 및 하위 탭 선택 후 데이터 조회</td></tr>
|
||||
<tr><td><strong>자산 등록</strong></td><td>[자산 추가] 버튼 클릭 후 상세 정보 입력 및 저장</td></tr>
|
||||
<tr><td><strong>정보 수정</strong></td><td>목록 행 클릭 후 나타나는 모달에서 내용 변경 및 저장</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 'pc',
|
||||
label: '💻 개인PC',
|
||||
content: `
|
||||
<section class="guide-section">
|
||||
<h3>개인PC 관리 가이드</h3>
|
||||
<p class="guide-text">
|
||||
임직원에게 지급되는 데스크톱 및 노트북을 관리합니다. 자산의 지급, 교체, 반납까지의 전체 생애주기를 시스템에서 추적합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>관리 프로세스</h3>
|
||||
<div class="flow-container">
|
||||
<div class="flow-row">
|
||||
<div class="flow-step">
|
||||
<span class="step-number">1</span>
|
||||
<div><span class="step-label">구매 및 입고</span><p class="step-desc">구매 요청 → 발주 → 입고 검수</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">2</span>
|
||||
<div><span class="step-label">자산 등록</span><p class="step-desc">자산번호 부여, 상세 사양 등록</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<i data-lucide="chevron-down" class="flow-arrow"></i>
|
||||
<div class="flow-row">
|
||||
<div class="flow-step">
|
||||
<span class="step-number">3</span>
|
||||
<div><span class="step-label">사용자 지급</span><p class="step-desc">사용자 지정 및 설치위치 기록</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">4</span>
|
||||
<div><span class="step-label">운영 관리</span><p class="step-desc">보안 점검 및 수리 이력 관리</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<i data-lucide="chevron-down" class="flow-arrow"></i>
|
||||
<div class="flow-row">
|
||||
<div class="flow-step">
|
||||
<span class="step-number">5</span>
|
||||
<div><span class="step-label">교체/반납</span><p class="step-desc">장비 회수 및 데이터 소거</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">6</span>
|
||||
<div><span class="step-label">폐기 처리</span><p class="step-desc">불용 처리 및 매각/폐기 등록</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>주요 관리 항목</h3>
|
||||
<table class="guide-info-table">
|
||||
<thead><tr><th>항목</th><th>설명</th><th>관리 주기</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>구매법인</td><td>자산의 소유 법인</td><td>등록 시</td></tr>
|
||||
<tr><td>사용자/조직</td><td>실제 사용자 및 소속 부서</td><td>변동 시</td></tr>
|
||||
<tr><td>자산번호</td><td>고유 식별 번호 (바코드)</td><td>등록 시</td></tr>
|
||||
<tr><td>모델명/사양</td><td>제조사 모델 및 CPU/RAM 등</td><td>등록 시</td></tr>
|
||||
<tr><td>구매금액</td><td>구매 비용 (부가세 포함)</td><td>등록 시</td></tr> </tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="guide-tip">
|
||||
<strong>관리 팁:</strong> 자산 이력에서 '분출'과 '반납' 로그를 꼼꼼히 기록하면 자산의 실제 위치를 정확히 파악할 수 있습니다.
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 'server',
|
||||
label: '🖥️ 서버/스토리지',
|
||||
content: `
|
||||
<section class="guide-section">
|
||||
<h3>인프라 자산 관리 가이드</h3>
|
||||
<p class="guide-text">
|
||||
서버실 및 IDC에 설치된 물리 서버와 스토리지 장비를 관리합니다. 고가의 자산이므로 담당자(정/부) 지정이 필수입니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>관리 프로세스</h3>
|
||||
<div class="flow-container">
|
||||
<div class="flow-row">
|
||||
<div class="flow-step">
|
||||
<span class="step-number">1</span>
|
||||
<div><span class="step-label">도입 계획</span><p class="step-desc">사양 확정 및 구매 승인</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">2</span>
|
||||
<div><span class="step-label">설치 및 등록</span><p class="step-desc">네트워크 설정 및 자산번호 부여</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">3</span>
|
||||
<div><span class="step-label">운영 관리</span><p class="step-desc">정기 점검 및 장애 이력 관리</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>필수 입력 항목</h3>
|
||||
<table class="guide-info-table">
|
||||
<thead><tr><th>항목</th><th>중요성</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>IP 주소</strong></td><td>서버 접속 및 모니터링을 위한 필수 정보</td></tr>
|
||||
<tr><td><strong>설치위치</strong></td><td>IDC 또는 서버실 내의 정확한 랙 위치</td></tr>
|
||||
<tr><td><strong>담당자(정/부)</strong></td><td>비상 시 연락 가능한 관리 책임자</td></tr>
|
||||
<tr><td><strong>용도/상세</strong></td><td>운영 중인 서비스 및 상세 업무 설명</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="guide-warn">
|
||||
<strong>주의 사항:</strong> 서버 자산의 IP가 변경될 경우 시스템에 즉시 반영하여 네트워크 관리 대장과의 정합성을 유지해야 합니다.
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 'software',
|
||||
label: '💾 소프트웨어',
|
||||
content: `
|
||||
<section class="guide-section">
|
||||
<h3>소프트웨어 자산 관리 가이드</h3>
|
||||
<p class="guide-text">
|
||||
구독형(SaaS) 및 영구형 라이선스를 관리합니다. 불법 소프트웨어 사용 방지와 비용 최적화가 주 목적입니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>라이선스 관리 포인트</h3>
|
||||
<table class="guide-info-table">
|
||||
<thead><tr><th>구분</th><th>관리 내용</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>구독형(Sub)</strong></td><td>구독 만료일 도래 전 갱신 여부 결정 및 비용 정산</td></tr>
|
||||
<tr><td><strong>영구형(Perm)</strong></td><td>보유 수량 대비 실제 설치 수량 매핑 (초과 사용 금지)</td></tr>
|
||||
<tr><td><strong>운영서비스</strong></td><td>도메인, 메일 등 매월 또는 매년 발생하는 비용 추적</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="guide-tip">
|
||||
<strong>팁:</strong> 소프트웨어 상세 페이지의 [사용자 할당] 기능을 활용하여 누가 어떤 라이선스를 사용하는지 체계적으로 관리하세요.
|
||||
</div>
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
// ─── 가이드 모달 초기화 ───
|
||||
export function initGuide() {
|
||||
const body = document.body;
|
||||
if (document.getElementById('guide-overlay')) return;
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay hidden';
|
||||
overlay.id = 'guide-overlay';
|
||||
|
||||
const tabsHtml = GUIDE_TABS.map((tab, i) =>
|
||||
`<div class="guide-tab ${i === 0 ? 'active' : ''}" data-guide-tab="${tab.id}">${tab.label}</div>`
|
||||
).join('');
|
||||
|
||||
const panelsHtml = GUIDE_TABS.map((tab, i) =>
|
||||
`<div class="guide-tab-panel ${i === 0 ? 'active' : ''}" data-guide-panel="${tab.id}">${tab.content}</div>`
|
||||
).join('');
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="modal-content wide" id="guide-modal" style="height: 90vh;">
|
||||
<div class="modal-header">
|
||||
<h2><i data-lucide="book-open"></i> 자산관리 프로세스 가이드 (Standard)</h2>
|
||||
<button class="btn-icon" id="btn-close-guide">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="guide-tabs-container">
|
||||
<div class="guide-tabs">${tabsHtml}</div>
|
||||
</div>
|
||||
<div class="modal-body" style="padding-top: 0;">
|
||||
<div class="guide-body">${panelsHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
body.appendChild(overlay);
|
||||
|
||||
const openGuide = () => {
|
||||
console.log('📖 Opening Full Guide Modal...');
|
||||
overlay.classList.remove('hidden');
|
||||
};
|
||||
const closeGuide = () => overlay.classList.add('hidden');
|
||||
|
||||
const triggerBtn = document.getElementById('btn-open-guide-header');
|
||||
if (triggerBtn) {
|
||||
triggerBtn.addEventListener('click', openGuide);
|
||||
}
|
||||
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeGuide(); });
|
||||
document.getElementById('btn-close-guide')?.addEventListener('click', closeGuide);
|
||||
|
||||
const tabs = overlay.querySelectorAll('.guide-tab');
|
||||
const panels = overlay.querySelectorAll('.guide-tab-panel');
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const targetId = tab.getAttribute('data-guide-tab');
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
panels.forEach(p => p.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
overlay.querySelector(`.guide-tab-panel[data-guide-panel="${targetId}"]`)?.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
createIcons({ icons: { BookOpen, X, ChevronDown, ChevronRight, RefreshCw } });
|
||||
}
|
||||
import { createIcons, BookOpen, X, ChevronDown, ChevronRight, RefreshCw } from 'lucide';
|
||||
import { state } from '../core/state';
|
||||
import './guide.css';
|
||||
|
||||
// ─── 자산별 가이드 콘텐츠 정의 (SW_Table 브랜치 전체 복구) ───
|
||||
interface GuideTabConfig {
|
||||
id: string;
|
||||
label: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const GUIDE_TABS: GuideTabConfig[] = [
|
||||
{
|
||||
id: 'overview',
|
||||
label: '📋 개요',
|
||||
content: `
|
||||
<section class="guide-section">
|
||||
<h3>IT 자산관리 시스템 개요</h3>
|
||||
<p class="guide-text">
|
||||
HM IT 자산관리 시스템(ITAM)은 기업의 IT 자산을 <strong>도입부터 폐기까지</strong> 전 과정에서 효율적으로 관리하기 위한 통합 플랫폼입니다.<br>
|
||||
하드웨어(PC, 서버, 스토리지, 전산비품, 모바일기기)와 소프트웨어(구독SW, 영구SW, 클라우드)를 체계적으로 추적하고 유지보수합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>전체 자산관리 프로세스</h3>
|
||||
<div class="flow-container">
|
||||
<div class="flow-row">
|
||||
<div class="flow-step">
|
||||
<span class="step-number">1</span>
|
||||
<div><span class="step-label">도입/구매</span><p class="step-desc">자산 구매 요청 → 승인 → 발주</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">2</span>
|
||||
<div><span class="step-label">등록/배정</span><p class="step-desc">자산번호 부여 → 시스템 등록 → 사용자 할당</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">3</span>
|
||||
<div><span class="step-label">운영/유지</span><p class="step-desc">현황 모니터링 → 점검/수리 → 이력 관리</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">4</span>
|
||||
<div><span class="step-label">반납/폐기</span><p class="step-desc">자산 회수 → 데이터 소거 → 폐기 처리</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>시스템 기본 사용방법</h3>
|
||||
<table class="guide-info-table">
|
||||
<thead><tr><th>기능</th><th>방법</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>자산 조회</strong></td><td>상단 카테고리(하드웨어/소프트웨어) 및 하위 탭 선택 후 데이터 조회</td></tr>
|
||||
<tr><td><strong>자산 등록</strong></td><td>[자산 추가] 버튼 클릭 후 상세 정보 입력 및 저장</td></tr>
|
||||
<tr><td><strong>정보 수정</strong></td><td>목록 행 클릭 후 나타나는 모달에서 내용 변경 및 저장</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 'pc',
|
||||
label: '💻 개인PC',
|
||||
content: `
|
||||
<section class="guide-section">
|
||||
<h3>개인PC 관리 가이드</h3>
|
||||
<p class="guide-text">
|
||||
임직원에게 지급되는 데스크톱 및 노트북을 관리합니다. 자산의 지급, 교체, 반납까지의 전체 생애주기를 시스템에서 추적합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>관리 프로세스</h3>
|
||||
<div class="flow-container">
|
||||
<div class="flow-row">
|
||||
<div class="flow-step">
|
||||
<span class="step-number">1</span>
|
||||
<div><span class="step-label">구매 및 입고</span><p class="step-desc">구매 요청 → 발주 → 입고 검수</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">2</span>
|
||||
<div><span class="step-label">자산 등록</span><p class="step-desc">자산번호 부여, 상세 사양 등록</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<i data-lucide="chevron-down" class="flow-arrow"></i>
|
||||
<div class="flow-row">
|
||||
<div class="flow-step">
|
||||
<span class="step-number">3</span>
|
||||
<div><span class="step-label">사용자 지급</span><p class="step-desc">사용자 지정 및 설치위치 기록</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">4</span>
|
||||
<div><span class="step-label">운영 관리</span><p class="step-desc">보안 점검 및 수리 이력 관리</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<i data-lucide="chevron-down" class="flow-arrow"></i>
|
||||
<div class="flow-row">
|
||||
<div class="flow-step">
|
||||
<span class="step-number">5</span>
|
||||
<div><span class="step-label">교체/반납</span><p class="step-desc">장비 회수 및 데이터 소거</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">6</span>
|
||||
<div><span class="step-label">폐기 처리</span><p class="step-desc">불용 처리 및 매각/폐기 등록</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>주요 관리 항목</h3>
|
||||
<table class="guide-info-table">
|
||||
<thead><tr><th>항목</th><th>설명</th><th>관리 주기</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>구매법인</td><td>자산의 소유 법인</td><td>등록 시</td></tr>
|
||||
<tr><td>사용자/조직</td><td>실제 사용자 및 소속 부서</td><td>변동 시</td></tr>
|
||||
<tr><td>자산번호</td><td>고유 식별 번호 (바코드)</td><td>등록 시</td></tr>
|
||||
<tr><td>모델명/사양</td><td>제조사 모델 및 CPU/RAM 등</td><td>등록 시</td></tr>
|
||||
<tr><td>구매금액</td><td>구매 비용 (부가세 포함)</td><td>등록 시</td></tr> </tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="guide-tip">
|
||||
<strong>관리 팁:</strong> 자산 이력에서 '분출'과 '반납' 로그를 꼼꼼히 기록하면 자산의 실제 위치를 정확히 파악할 수 있습니다.
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 'server',
|
||||
label: '🖥️ 서버/스토리지',
|
||||
content: `
|
||||
<section class="guide-section">
|
||||
<h3>인프라 자산 관리 가이드</h3>
|
||||
<p class="guide-text">
|
||||
서버실 및 IDC에 설치된 물리 서버와 스토리지 장비를 관리합니다. 고가의 자산이므로 담당자(정/부) 지정이 필수입니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>관리 프로세스</h3>
|
||||
<div class="flow-container">
|
||||
<div class="flow-row">
|
||||
<div class="flow-step">
|
||||
<span class="step-number">1</span>
|
||||
<div><span class="step-label">도입 계획</span><p class="step-desc">사양 확정 및 구매 승인</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">2</span>
|
||||
<div><span class="step-label">설치 및 등록</span><p class="step-desc">네트워크 설정 및 자산번호 부여</p></div>
|
||||
</div>
|
||||
<span class="flow-arrow-right"><i data-lucide="chevron-right"></i></span>
|
||||
<div class="flow-step">
|
||||
<span class="step-number">3</span>
|
||||
<div><span class="step-label">운영 관리</span><p class="step-desc">정기 점검 및 장애 이력 관리</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>필수 입력 항목</h3>
|
||||
<table class="guide-info-table">
|
||||
<thead><tr><th>항목</th><th>중요성</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>IP 주소</strong></td><td>서버 접속 및 모니터링을 위한 필수 정보</td></tr>
|
||||
<tr><td><strong>설치위치</strong></td><td>IDC 또는 서버실 내의 정확한 랙 위치</td></tr>
|
||||
<tr><td><strong>담당자(정/부)</strong></td><td>비상 시 연락 가능한 관리 책임자</td></tr>
|
||||
<tr><td><strong>용도/상세</strong></td><td>운영 중인 서비스 및 상세 업무 설명</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="guide-warn">
|
||||
<strong>주의 사항:</strong> 서버 자산의 IP가 변경될 경우 시스템에 즉시 반영하여 네트워크 관리 대장과의 정합성을 유지해야 합니다.
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
id: 'software',
|
||||
label: '💾 소프트웨어',
|
||||
content: `
|
||||
<section class="guide-section">
|
||||
<h3>소프트웨어 자산 관리 가이드</h3>
|
||||
<p class="guide-text">
|
||||
구독형(SaaS) 및 영구형 라이선스를 관리합니다. 불법 소프트웨어 사용 방지와 비용 최적화가 주 목적입니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="guide-section">
|
||||
<h3>라이선스 관리 포인트</h3>
|
||||
<table class="guide-info-table">
|
||||
<thead><tr><th>구분</th><th>관리 내용</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>구독형(Sub)</strong></td><td>구독 만료일 도래 전 갱신 여부 결정 및 비용 정산</td></tr>
|
||||
<tr><td><strong>영구형(Perm)</strong></td><td>보유 수량 대비 실제 설치 수량 매핑 (초과 사용 금지)</td></tr>
|
||||
<tr><td><strong>운영서비스</strong></td><td>도메인, 메일 등 매월 또는 매년 발생하는 비용 추적</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="guide-tip">
|
||||
<strong>팁:</strong> 소프트웨어 상세 페이지의 [사용자 할당] 기능을 활용하여 누가 어떤 라이선스를 사용하는지 체계적으로 관리하세요.
|
||||
</div>
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
// ─── 가이드 모달 초기화 ───
|
||||
export function initGuide() {
|
||||
const body = document.body;
|
||||
if (document.getElementById('guide-overlay')) return;
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay hidden';
|
||||
overlay.id = 'guide-overlay';
|
||||
|
||||
const tabsHtml = GUIDE_TABS.map((tab, i) =>
|
||||
`<div class="guide-tab ${i === 0 ? 'active' : ''}" data-guide-tab="${tab.id}">${tab.label}</div>`
|
||||
).join('');
|
||||
|
||||
const panelsHtml = GUIDE_TABS.map((tab, i) =>
|
||||
`<div class="guide-tab-panel ${i === 0 ? 'active' : ''}" data-guide-panel="${tab.id}">${tab.content}</div>`
|
||||
).join('');
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="modal-content wide" id="guide-modal" style="height: 90vh;">
|
||||
<div class="modal-header">
|
||||
<h2><i data-lucide="book-open"></i> 자산관리 프로세스 가이드 (Standard)</h2>
|
||||
<button class="btn-icon" id="btn-close-guide">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="guide-tabs-container">
|
||||
<div class="guide-tabs">${tabsHtml}</div>
|
||||
</div>
|
||||
<div class="modal-body" style="padding-top: 0;">
|
||||
<div class="guide-body">${panelsHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
body.appendChild(overlay);
|
||||
|
||||
const openGuide = () => {
|
||||
console.log('📖 Opening Full Guide Modal...');
|
||||
overlay.classList.remove('hidden');
|
||||
};
|
||||
const closeGuide = () => overlay.classList.add('hidden');
|
||||
|
||||
const triggerBtn = document.getElementById('btn-open-guide-header');
|
||||
if (triggerBtn) {
|
||||
triggerBtn.addEventListener('click', openGuide);
|
||||
}
|
||||
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeGuide(); });
|
||||
document.getElementById('btn-close-guide')?.addEventListener('click', closeGuide);
|
||||
|
||||
const tabs = overlay.querySelectorAll('.guide-tab');
|
||||
const panels = overlay.querySelectorAll('.guide-tab-panel');
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const targetId = tab.getAttribute('data-guide-tab');
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
panels.forEach(p => p.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
overlay.querySelector(`.guide-tab-panel[data-guide-panel="${targetId}"]`)?.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
createIcons({ icons: { BookOpen, X, ChevronDown, ChevronRight, RefreshCw } });
|
||||
}
|
||||
|
||||
@@ -1,143 +1,143 @@
|
||||
import { createIcons, X } from 'lucide';
|
||||
import { setEditLock } from './ModalUtils';
|
||||
import './modal.css';
|
||||
|
||||
/**
|
||||
* 모든 모달의 공통 기능을 관리하는 베이스 추상 클래스입니다.
|
||||
*/
|
||||
export abstract class BaseModal {
|
||||
protected idPrefix: string;
|
||||
protected title: string;
|
||||
protected currentAsset: any | null = null;
|
||||
protected isEditMode: boolean = false;
|
||||
protected currentMode: 'view' | 'edit' | 'add' = 'view';
|
||||
protected modalEl: HTMLElement | null = null;
|
||||
protected formEl: HTMLFormElement | null = null;
|
||||
|
||||
constructor(idPrefix: string, title: string) {
|
||||
this.idPrefix = idPrefix;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 초기화: HTML 삽입 및 공통 이벤트 바인딩
|
||||
*/
|
||||
public init(onSave: () => void, closeModalsFn: () => void) {
|
||||
// 1. 프레임 HTML 삽입 (자식 클래스에서 정의한 HTML 사용)
|
||||
if (!document.getElementById(`${this.idPrefix}-asset-modal`)) {
|
||||
document.body.insertAdjacentHTML('beforeend', this.renderFrameHTML());
|
||||
}
|
||||
|
||||
this.modalEl = document.getElementById(`${this.idPrefix}-asset-modal`);
|
||||
this.formEl = document.getElementById(`${this.idPrefix}-asset-form`) as HTMLFormElement;
|
||||
|
||||
// 2. 공통 버튼 이벤트 바인딩 (닫기, 취소 등)
|
||||
const btnCloseHeader = document.getElementById(`btn-close-${this.idPrefix}-modal`);
|
||||
const btnCancelFooter = document.getElementById(`btn-cancel-${this.idPrefix}-modal`);
|
||||
|
||||
const closeAction = () => {
|
||||
this.close();
|
||||
closeModalsFn(); // 전역 모달 상태 해제 콜백
|
||||
};
|
||||
|
||||
btnCloseHeader?.addEventListener('click', closeAction);
|
||||
btnCancelFooter?.addEventListener('click', closeAction);
|
||||
|
||||
// 3. 자식 클래스 전용 초기화 로직 실행
|
||||
this.initChildLogic(onSave, closeModalsFn);
|
||||
|
||||
// 4. 아이콘 초기화
|
||||
createIcons({ icons: { X } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 열기: 데이터 바인딩 및 모드 설정
|
||||
*/
|
||||
public open(asset: any, mode: 'view' | 'edit' | 'add' = 'view') {
|
||||
this.currentAsset = asset;
|
||||
this.currentMode = mode;
|
||||
this.isEditMode = (mode === 'add' || mode === 'edit');
|
||||
|
||||
// 폼 초기화 추가
|
||||
if (this.formEl) this.formEl.reset();
|
||||
|
||||
// fillFormData를 먼저 호출하여 동적 요소들을 생성한 후 잠금 처리
|
||||
this.fillFormData(asset);
|
||||
this.setEditLockMode(mode);
|
||||
|
||||
if (this.modalEl) {
|
||||
this.modalEl.classList.remove('hidden');
|
||||
const content = this.modalEl.querySelector('.modal-content');
|
||||
if (content) {
|
||||
if (mode === 'view') content.classList.add('is-view-mode');
|
||||
else content.classList.remove('is-view-mode');
|
||||
}
|
||||
}
|
||||
|
||||
this.onAfterOpen(asset, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 닫기: 상태 초기화
|
||||
*/
|
||||
public close() {
|
||||
if (this.modalEl) {
|
||||
this.modalEl.classList.add('hidden');
|
||||
}
|
||||
this.isEditMode = false;
|
||||
this.currentAsset = null;
|
||||
this.onAfterClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 조회/수정 모드에 따른 UI 잠금 및 버튼 제어
|
||||
*/
|
||||
protected setEditLockMode(mode: 'view' | 'edit' | 'add') {
|
||||
setEditLock(`${this.idPrefix}-asset-form`, mode, {
|
||||
saveBtnId: `btn-save-${this.idPrefix}-asset`,
|
||||
revertBtnId: `btn-revert-${this.idPrefix}-edit`,
|
||||
addLogBtnId: `btn-add-${this.idPrefix}-log`
|
||||
});
|
||||
}
|
||||
|
||||
// --- 추상 메서드: 자식 클래스에서 구현해야 함 ---
|
||||
protected abstract renderFrameHTML(): string;
|
||||
protected abstract initChildLogic(onSave: () => void, closeModals: () => void): void;
|
||||
protected abstract fillFormData(asset: any): void;
|
||||
protected abstract onAfterOpen(asset: any, mode: string): void;
|
||||
|
||||
// --- 훅(Hook) 메서드: 필요 시 오버라이드 ---
|
||||
protected onAfterClose(): void {}
|
||||
}
|
||||
|
||||
/**
|
||||
* --- 레거시 호환성을 위한 함수형 익스포트 ---
|
||||
* 기존 코드들이 참조하고 있는 함수들을 유지합니다.
|
||||
*/
|
||||
export function closeModals() {
|
||||
const modals = document.querySelectorAll('.modal-overlay');
|
||||
modals.forEach(modal => modal.classList.add('hidden'));
|
||||
}
|
||||
|
||||
export function initBaseModal() {
|
||||
// ESC 키로 모든 모달 닫기 (위치보기 팝업이 있으면 그것부터 닫음)
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const picker = document.querySelector('.image-picker-overlay');
|
||||
if (picker) {
|
||||
picker.remove();
|
||||
} else {
|
||||
closeModals();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { closeAllModals: closeModals };
|
||||
}
|
||||
|
||||
export function openModal(modalId: string) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
import { createIcons, X } from 'lucide';
|
||||
import { setEditLock } from './ModalUtils';
|
||||
import './modal.css';
|
||||
|
||||
/**
|
||||
* 모든 모달의 공통 기능을 관리하는 베이스 추상 클래스입니다.
|
||||
*/
|
||||
export abstract class BaseModal {
|
||||
protected idPrefix: string;
|
||||
protected title: string;
|
||||
protected currentAsset: any | null = null;
|
||||
protected isEditMode: boolean = false;
|
||||
protected currentMode: 'view' | 'edit' | 'add' = 'view';
|
||||
protected modalEl: HTMLElement | null = null;
|
||||
protected formEl: HTMLFormElement | null = null;
|
||||
|
||||
constructor(idPrefix: string, title: string) {
|
||||
this.idPrefix = idPrefix;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 초기화: HTML 삽입 및 공통 이벤트 바인딩
|
||||
*/
|
||||
public init(onSave: () => void, closeModalsFn: () => void) {
|
||||
// 1. 프레임 HTML 삽입 (자식 클래스에서 정의한 HTML 사용)
|
||||
if (!document.getElementById(`${this.idPrefix}-asset-modal`)) {
|
||||
document.body.insertAdjacentHTML('beforeend', this.renderFrameHTML());
|
||||
}
|
||||
|
||||
this.modalEl = document.getElementById(`${this.idPrefix}-asset-modal`);
|
||||
this.formEl = document.getElementById(`${this.idPrefix}-asset-form`) as HTMLFormElement;
|
||||
|
||||
// 2. 공통 버튼 이벤트 바인딩 (닫기, 취소 등)
|
||||
const btnCloseHeader = document.getElementById(`btn-close-${this.idPrefix}-modal`);
|
||||
const btnCancelFooter = document.getElementById(`btn-cancel-${this.idPrefix}-modal`);
|
||||
|
||||
const closeAction = () => {
|
||||
this.close();
|
||||
closeModalsFn(); // 전역 모달 상태 해제 콜백
|
||||
};
|
||||
|
||||
btnCloseHeader?.addEventListener('click', closeAction);
|
||||
btnCancelFooter?.addEventListener('click', closeAction);
|
||||
|
||||
// 3. 자식 클래스 전용 초기화 로직 실행
|
||||
this.initChildLogic(onSave, closeModalsFn);
|
||||
|
||||
// 4. 아이콘 초기화
|
||||
createIcons({ icons: { X } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 열기: 데이터 바인딩 및 모드 설정
|
||||
*/
|
||||
public open(asset: any, mode: 'view' | 'edit' | 'add' = 'view') {
|
||||
this.currentAsset = asset;
|
||||
this.currentMode = mode;
|
||||
this.isEditMode = (mode === 'add' || mode === 'edit');
|
||||
|
||||
// 폼 초기화 추가
|
||||
if (this.formEl) this.formEl.reset();
|
||||
|
||||
// fillFormData를 먼저 호출하여 동적 요소들을 생성한 후 잠금 처리
|
||||
this.fillFormData(asset);
|
||||
this.setEditLockMode(mode);
|
||||
|
||||
if (this.modalEl) {
|
||||
this.modalEl.classList.remove('hidden');
|
||||
const content = this.modalEl.querySelector('.modal-content');
|
||||
if (content) {
|
||||
if (mode === 'view') content.classList.add('is-view-mode');
|
||||
else content.classList.remove('is-view-mode');
|
||||
}
|
||||
}
|
||||
|
||||
this.onAfterOpen(asset, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 닫기: 상태 초기화
|
||||
*/
|
||||
public close() {
|
||||
if (this.modalEl) {
|
||||
this.modalEl.classList.add('hidden');
|
||||
}
|
||||
this.isEditMode = false;
|
||||
this.currentAsset = null;
|
||||
this.onAfterClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 조회/수정 모드에 따른 UI 잠금 및 버튼 제어
|
||||
*/
|
||||
protected setEditLockMode(mode: 'view' | 'edit' | 'add') {
|
||||
setEditLock(`${this.idPrefix}-asset-form`, mode, {
|
||||
saveBtnId: `btn-save-${this.idPrefix}-asset`,
|
||||
revertBtnId: `btn-revert-${this.idPrefix}-edit`,
|
||||
addLogBtnId: `btn-add-${this.idPrefix}-log`
|
||||
});
|
||||
}
|
||||
|
||||
// --- 추상 메서드: 자식 클래스에서 구현해야 함 ---
|
||||
protected abstract renderFrameHTML(): string;
|
||||
protected abstract initChildLogic(onSave: () => void, closeModals: () => void): void;
|
||||
protected abstract fillFormData(asset: any): void;
|
||||
protected abstract onAfterOpen(asset: any, mode: string): void;
|
||||
|
||||
// --- 훅(Hook) 메서드: 필요 시 오버라이드 ---
|
||||
protected onAfterClose(): void {}
|
||||
}
|
||||
|
||||
/**
|
||||
* --- 레거시 호환성을 위한 함수형 익스포트 ---
|
||||
* 기존 코드들이 참조하고 있는 함수들을 유지합니다.
|
||||
*/
|
||||
export function closeModals() {
|
||||
const modals = document.querySelectorAll('.modal-overlay');
|
||||
modals.forEach(modal => modal.classList.add('hidden'));
|
||||
}
|
||||
|
||||
export function initBaseModal() {
|
||||
// ESC 키로 모든 모달 닫기 (위치보기 팝업이 있으면 그것부터 닫음)
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const picker = document.querySelector('.image-picker-overlay');
|
||||
if (picker) {
|
||||
picker.remove();
|
||||
} else {
|
||||
closeModals();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { closeAllModals: closeModals };
|
||||
}
|
||||
|
||||
export function openModal(modalId: string) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +1,136 @@
|
||||
import { state } from '../../core/state';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createIcons, X } from 'lucide';
|
||||
|
||||
const DASHBOARD_DETAIL_MODAL_HTML = `
|
||||
<div id="dashboard-detail-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<h2 id="dashboard-detail-modal-title" class="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>
|
||||
<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>
|
||||
`;
|
||||
|
||||
export function initDashboardDetailModal() {
|
||||
if (!document.getElementById('dashboard-detail-modal')) {
|
||||
document.body.insertAdjacentHTML('beforeend', DASHBOARD_DETAIL_MODAL_HTML);
|
||||
}
|
||||
|
||||
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(); });
|
||||
|
||||
createIcons({ icons: { X } });
|
||||
}
|
||||
|
||||
export function openDashboardDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||
if (!titleEl || !tbody) return;
|
||||
const thead = tbody.closest('table')?.querySelector('thead');
|
||||
if (!thead) return;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</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="7" style="text-align:center; padding: 2rem;">해당 조건의 자산이 없습니다.</td></tr>`;
|
||||
} else {
|
||||
list.forEach((asset, idx) => {
|
||||
let manager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || asset.user_current || '-';
|
||||
let name = asset[ASSET_SCHEMA.MODEL_NAME.key] || asset[ASSET_SCHEMA.ASSET_NAME.key] || '-';
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.category || asset[ASSET_SCHEMA.ASSET_TYPE.key]}</td><td>${name}</td><td>${asset[ASSET_SCHEMA.LOCATION.key]||'-'}</td><td>${manager}</td><td>${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||'-'}</td><td>${asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key]||'-'}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
export function openSwDashboardDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||
if (!titleEl || !tbody) return;
|
||||
const thead = tbody.closest('table')?.querySelector('thead');
|
||||
if (!thead) return;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>법인</th><th>제품명</th><th>수량</th><th>금액</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
list.forEach((sw, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw.asset_type || sw.type}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]}</td><td>${sw[ASSET_SCHEMA.ASSET_COUNT.key]}</td><td>${sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
export function openSwUsageDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||
if (!titleEl || !tbody) return;
|
||||
const thead = tbody.closest('table')?.querySelector('thead');
|
||||
if (!thead) return;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</th><th>법인</th><th>제품명</th><th>수량</th><th>사용중</th><th>사용가능</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
list.forEach((sw, idx) => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length;
|
||||
const qty = Number(sw[ASSET_SCHEMA.ASSET_COUNT.key] || 0);
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]}</td><td>${qty}</td><td>${assigned}</td><td>${qty - assigned}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
export function openCloudDashboardDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||
if (!titleEl || !tbody) return;
|
||||
const thead = tbody.closest('table')?.querySelector('thead');
|
||||
if (!thead) return;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</th><th>플랫폼/목적</th><th>법인</th><th>제품명</th><th>결제일</th><th>당월청구액(원)</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
if (list.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; padding: 2rem;">해당 내역이 없습니다.</td></tr>`;
|
||||
} else {
|
||||
list.forEach((sw, idx) => {
|
||||
const priceStr = sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? Number(String(sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/[^0-9]/g, '')).toLocaleString() : '0';
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw[ASSET_SCHEMA.DEV_OBJ.key]||'-'}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]||'-'}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]||'-'}</td><td>${sw.pay_day ? sw.pay_day + '일' : '-'}</td><td>₩ ${priceStr}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createIcons, X } from 'lucide';
|
||||
|
||||
const DASHBOARD_DETAIL_MODAL_HTML = `
|
||||
<div id="dashboard-detail-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<h2 id="dashboard-detail-modal-title" class="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>
|
||||
<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>
|
||||
`;
|
||||
|
||||
export function initDashboardDetailModal() {
|
||||
if (!document.getElementById('dashboard-detail-modal')) {
|
||||
document.body.insertAdjacentHTML('beforeend', DASHBOARD_DETAIL_MODAL_HTML);
|
||||
}
|
||||
|
||||
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(); });
|
||||
|
||||
createIcons({ icons: { X } });
|
||||
}
|
||||
|
||||
export function openDashboardDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||
if (!titleEl || !tbody) return;
|
||||
const thead = tbody.closest('table')?.querySelector('thead');
|
||||
if (!thead) return;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</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="7" style="text-align:center; padding: 2rem;">해당 조건의 자산이 없습니다.</td></tr>`;
|
||||
} else {
|
||||
list.forEach((asset, idx) => {
|
||||
let manager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || asset.user_current || '-';
|
||||
let name = asset[ASSET_SCHEMA.MODEL_NAME.key] || asset[ASSET_SCHEMA.ASSET_NAME.key] || '-';
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${asset.category || asset[ASSET_SCHEMA.ASSET_TYPE.key]}</td><td>${name}</td><td>${asset[ASSET_SCHEMA.LOCATION.key]||'-'}</td><td>${manager}</td><td>${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||'-'}</td><td>${asset[ASSET_SCHEMA.PURCHASE_AMOUNT.key]||'-'}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
export function openSwDashboardDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||
if (!titleEl || !tbody) return;
|
||||
const thead = tbody.closest('table')?.querySelector('thead');
|
||||
if (!thead) return;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</th><th>유형</th><th>법인</th><th>제품명</th><th>수량</th><th>금액</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
list.forEach((sw, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw.asset_type || sw.type}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]}</td><td>${sw[ASSET_SCHEMA.ASSET_COUNT.key]}</td><td>${sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
export function openSwUsageDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||
if (!titleEl || !tbody) return;
|
||||
const thead = tbody.closest('table')?.querySelector('thead');
|
||||
if (!thead) return;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</th><th>법인</th><th>제품명</th><th>수량</th><th>사용중</th><th>사용가능</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
list.forEach((sw, idx) => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length;
|
||||
const qty = Number(sw[ASSET_SCHEMA.ASSET_COUNT.key] || 0);
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]}</td><td>${qty}</td><td>${assigned}</td><td>${qty - assigned}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
export function openCloudDashboardDetail(title: string, list: any[]) {
|
||||
const modal = document.getElementById('dashboard-detail-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('dashboard-detail-modal-title');
|
||||
const tbody = document.getElementById('dashboard-detail-tbody');
|
||||
if (!titleEl || !tbody) return;
|
||||
const thead = tbody.closest('table')?.querySelector('thead');
|
||||
if (!thead) return;
|
||||
|
||||
titleEl.textContent = title;
|
||||
thead.innerHTML = `<tr><th>No</th><th>플랫폼/목적</th><th>법인</th><th>제품명</th><th>결제일</th><th>당월청구액(원)</th></tr>`;
|
||||
tbody.innerHTML = '';
|
||||
if (list.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; padding: 2rem;">해당 내역이 없습니다.</td></tr>`;
|
||||
} else {
|
||||
list.forEach((sw, idx) => {
|
||||
const priceStr = sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? Number(String(sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/[^0-9]/g, '')).toLocaleString() : '0';
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${idx+1}</td><td>${sw[ASSET_SCHEMA.DEV_OBJ.key]||'-'}</td><td>${sw[ASSET_SCHEMA.PURCHASE_CORP.key]||'-'}</td><td>${sw[ASSET_SCHEMA.PRODUCT_NAME.key]||'-'}</td><td>${sw.pay_day ? sw.pay_day + '일' : '-'}</td><td>₩ ${priceStr}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
@@ -1,212 +1,212 @@
|
||||
import { state, saveAsset, deleteAsset } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { CORP_LIST } from './SharedData';
|
||||
import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils';
|
||||
import { createIcons, X, Save, History, Plus } from 'lucide';
|
||||
import { formatExcelDate } from '../../core/excelHandler';
|
||||
import { UI_TEXT } from '../../core/schema';
|
||||
|
||||
class DomainAssetModal extends BaseModal {
|
||||
constructor() {
|
||||
super('domain', '도메인 정보');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="domain-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<h2 id="domain-modal-title" class="modal-title">${this.title}</h2>
|
||||
<div id="domain-header-identity" class="header-identity"></div>
|
||||
</div>
|
||||
<button id="btn-close-domain-modal" class="btn-icon" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body-split">
|
||||
<div class="modal-form-area">
|
||||
<form id="domain-asset-form" class="grid-form">
|
||||
<input type="hidden" id="domain-id" name="id" />
|
||||
|
||||
<div class="form-section-title">기본 정보</div>
|
||||
<div class="form-group">
|
||||
<label>구분</label>
|
||||
<select id="domain-type" name="type">
|
||||
<option value="호스팅">호스팅</option>
|
||||
<option value="도메인">도메인</option>
|
||||
<option value="기타">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>관리법인</label>
|
||||
<select id="domain-corp" name="corp">${generateOptionsHTML(CORP_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>서비스명</label>
|
||||
<input type="text" id="domain-service-name" name="service_name" required />
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>관리도메인</label>
|
||||
<input type="text" id="domain-name" name="domain_name" required />
|
||||
</div>
|
||||
|
||||
<div class="form-section-title">계약 및 비용</div>
|
||||
<div class="form-group">
|
||||
<label>계약시작일</label>
|
||||
<input type="date" id="domain-start-date" name="start_date" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>만료예정일</label>
|
||||
<input type="date" id="domain-expiry-date" name="expiry_date" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>비용 (연간/월간)</label>
|
||||
<input type="text" id="domain-price" name="price" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
||||
</div>
|
||||
|
||||
<div class="form-section-title">담당자 및 비고</div>
|
||||
<div class="form-group">
|
||||
<label>정담당자</label>
|
||||
<input type="text" id="domain-manager-main" name="manager_main" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>부담당자</label>
|
||||
<input type="text" id="domain-manager-sub" name="manager_sub" />
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>비고</label>
|
||||
<textarea id="domain-remarks" name="remarks" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-history-area">
|
||||
<div class="history-header">
|
||||
<h3><i data-lucide="history"></i> 변경 이력</h3>
|
||||
<button type="button" id="btn-add-domain-log" class="btn btn-outline btn-sm">
|
||||
이력 추가 <i data-lucide="plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="domain-history-list" class="history-timeline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-domain-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-revert-domain-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-cancel-domain-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-domain-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const saveBtn = document.getElementById('btn-save-domain-asset')!;
|
||||
const revertBtn = document.getElementById('btn-revert-domain-edit')!;
|
||||
const deleteBtn = document.getElementById('btn-delete-domain-asset')!;
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset) return;
|
||||
if (!this.isEditMode) {
|
||||
this.setEditLockMode('edit');
|
||||
this.isEditMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(this.formEl!);
|
||||
const updated = { ...this.currentAsset };
|
||||
formData.forEach((value, key) => { updated[key] = value; });
|
||||
|
||||
if (!updated.service_name || !updated.domain_name) {
|
||||
alert('서비스명과 관리도메인은 필수 입력 사항입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (await saveAsset('domain', updated)) {
|
||||
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
revertBtn.addEventListener('click', () => {
|
||||
this.setEditLockMode('view');
|
||||
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset || !confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return;
|
||||
if (await deleteAsset('domain', this.currentAsset.id)) {
|
||||
alert('성공적으로 삭제되었습니다.');
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
createIcons({ icons: { History, Plus, Save, X } });
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
setFieldValue('domain-id', asset.id);
|
||||
setFieldValue('domain-type', asset.type || '호스팅');
|
||||
setFieldValue('domain-corp', asset.corp || '');
|
||||
setFieldValue('domain-service-name', asset.service_name || '');
|
||||
setFieldValue('domain-name', asset.domain_name || '');
|
||||
setFieldValue('domain-start-date', formatExcelDate(asset.start_date));
|
||||
setFieldValue('domain-expiry-date', formatExcelDate(asset.expiry_date));
|
||||
setFieldValue('domain-price', asset.price || '');
|
||||
setFieldValue('domain-manager-main', asset.manager_main || '');
|
||||
setFieldValue('domain-manager-sub', asset.manager_sub || '');
|
||||
setFieldValue('domain-remarks', asset.remarks || '');
|
||||
|
||||
this.renderHistory(asset.id);
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
protected onAfterOpen(asset: any, mode: string): void {
|
||||
const titleEl = document.getElementById('domain-modal-title');
|
||||
if (titleEl) titleEl.textContent = (mode === 'add') ? '신규 도메인 등록' : '도메인 정보 상세';
|
||||
|
||||
const deleteBtn = document.getElementById('btn-delete-domain-asset');
|
||||
if (deleteBtn) deleteBtn.style.display = (mode === 'add') ? 'none' : 'block';
|
||||
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
private updateHeaderIdentity(asset: any) {
|
||||
const container = document.getElementById('domain-header-identity');
|
||||
if (!container) return;
|
||||
|
||||
if (this.currentMode === 'add') {
|
||||
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const type = getFieldValue('domain-type') || asset.type || '';
|
||||
const serviceName = getFieldValue('domain-service-name') || asset.service_name || '';
|
||||
const domainName = getFieldValue('domain-name') || asset.domain_name || '';
|
||||
|
||||
container.innerHTML = `
|
||||
<span class="asset-code-title">${serviceName}</span>
|
||||
<span class="service-type-badge">${type}</span>
|
||||
<span class="asset-type-label">${domainName}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderHistory(assetId: string) {
|
||||
const container = document.getElementById('domain-history-list');
|
||||
if (!container) return;
|
||||
|
||||
const logs = (state.masterData.logs || []).filter(l => l.asset_id === assetId);
|
||||
if (logs.length === 0) {
|
||||
container.innerHTML = '<div style="color:var(--mute); padding:1rem; text-align:center;">이력이 없습니다.</div>';
|
||||
} else {
|
||||
container.innerHTML = logs.map(l => `<div class="history-item"><div class="history-date">${l.log_date || ''}</div><div class="history-user">${l.log_user || '시스템'}</div><div class="history-details">${l.details}</div></div>`).join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const domainModal = new DomainAssetModal();
|
||||
export function initDomainModal(onSave: () => void, closeModals: () => void) { domainModal.init(onSave, closeModals); }
|
||||
export function openDomainModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') { domainModal.open(asset, mode); }
|
||||
import { state, saveAsset, deleteAsset } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { CORP_LIST } from './SharedData';
|
||||
import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils';
|
||||
import { createIcons, X, Save, History, Plus } from 'lucide';
|
||||
import { formatExcelDate } from '../../core/excelHandler';
|
||||
import { UI_TEXT } from '../../core/schema';
|
||||
|
||||
class DomainAssetModal extends BaseModal {
|
||||
constructor() {
|
||||
super('domain', '도메인 정보');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="domain-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<h2 id="domain-modal-title" class="modal-title">${this.title}</h2>
|
||||
<div id="domain-header-identity" class="header-identity"></div>
|
||||
</div>
|
||||
<button id="btn-close-domain-modal" class="btn-icon" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body-split">
|
||||
<div class="modal-form-area">
|
||||
<form id="domain-asset-form" class="grid-form">
|
||||
<input type="hidden" id="domain-id" name="id" />
|
||||
|
||||
<div class="form-section-title">기본 정보</div>
|
||||
<div class="form-group">
|
||||
<label>구분</label>
|
||||
<select id="domain-type" name="type">
|
||||
<option value="호스팅">호스팅</option>
|
||||
<option value="도메인">도메인</option>
|
||||
<option value="기타">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>관리법인</label>
|
||||
<select id="domain-corp" name="corp">${generateOptionsHTML(CORP_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>서비스명</label>
|
||||
<input type="text" id="domain-service-name" name="service_name" required />
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>관리도메인</label>
|
||||
<input type="text" id="domain-name" name="domain_name" required />
|
||||
</div>
|
||||
|
||||
<div class="form-section-title">계약 및 비용</div>
|
||||
<div class="form-group">
|
||||
<label>계약시작일</label>
|
||||
<input type="date" id="domain-start-date" name="start_date" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>만료예정일</label>
|
||||
<input type="date" id="domain-expiry-date" name="expiry_date" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>비용 (연간/월간)</label>
|
||||
<input type="text" id="domain-price" name="price" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
||||
</div>
|
||||
|
||||
<div class="form-section-title">담당자 및 비고</div>
|
||||
<div class="form-group">
|
||||
<label>정담당자</label>
|
||||
<input type="text" id="domain-manager-main" name="manager_main" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>부담당자</label>
|
||||
<input type="text" id="domain-manager-sub" name="manager_sub" />
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>비고</label>
|
||||
<textarea id="domain-remarks" name="remarks" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-history-area">
|
||||
<div class="history-header">
|
||||
<h3><i data-lucide="history"></i> 변경 이력</h3>
|
||||
<button type="button" id="btn-add-domain-log" class="btn btn-outline btn-sm">
|
||||
이력 추가 <i data-lucide="plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="domain-history-list" class="history-timeline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-domain-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-revert-domain-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-cancel-domain-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-domain-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const saveBtn = document.getElementById('btn-save-domain-asset')!;
|
||||
const revertBtn = document.getElementById('btn-revert-domain-edit')!;
|
||||
const deleteBtn = document.getElementById('btn-delete-domain-asset')!;
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset) return;
|
||||
if (!this.isEditMode) {
|
||||
this.setEditLockMode('edit');
|
||||
this.isEditMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(this.formEl!);
|
||||
const updated = { ...this.currentAsset };
|
||||
formData.forEach((value, key) => { updated[key] = value; });
|
||||
|
||||
if (!updated.service_name || !updated.domain_name) {
|
||||
alert('서비스명과 관리도메인은 필수 입력 사항입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (await saveAsset('domain', updated)) {
|
||||
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
revertBtn.addEventListener('click', () => {
|
||||
this.setEditLockMode('view');
|
||||
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset || !confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return;
|
||||
if (await deleteAsset('domain', this.currentAsset.id)) {
|
||||
alert('성공적으로 삭제되었습니다.');
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
createIcons({ icons: { History, Plus, Save, X } });
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
setFieldValue('domain-id', asset.id);
|
||||
setFieldValue('domain-type', asset.type || '호스팅');
|
||||
setFieldValue('domain-corp', asset.corp || '');
|
||||
setFieldValue('domain-service-name', asset.service_name || '');
|
||||
setFieldValue('domain-name', asset.domain_name || '');
|
||||
setFieldValue('domain-start-date', formatExcelDate(asset.start_date));
|
||||
setFieldValue('domain-expiry-date', formatExcelDate(asset.expiry_date));
|
||||
setFieldValue('domain-price', asset.price || '');
|
||||
setFieldValue('domain-manager-main', asset.manager_main || '');
|
||||
setFieldValue('domain-manager-sub', asset.manager_sub || '');
|
||||
setFieldValue('domain-remarks', asset.remarks || '');
|
||||
|
||||
this.renderHistory(asset.id);
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
protected onAfterOpen(asset: any, mode: string): void {
|
||||
const titleEl = document.getElementById('domain-modal-title');
|
||||
if (titleEl) titleEl.textContent = (mode === 'add') ? '신규 도메인 등록' : '도메인 정보 상세';
|
||||
|
||||
const deleteBtn = document.getElementById('btn-delete-domain-asset');
|
||||
if (deleteBtn) deleteBtn.style.display = (mode === 'add') ? 'none' : 'block';
|
||||
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
private updateHeaderIdentity(asset: any) {
|
||||
const container = document.getElementById('domain-header-identity');
|
||||
if (!container) return;
|
||||
|
||||
if (this.currentMode === 'add') {
|
||||
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const type = getFieldValue('domain-type') || asset.type || '';
|
||||
const serviceName = getFieldValue('domain-service-name') || asset.service_name || '';
|
||||
const domainName = getFieldValue('domain-name') || asset.domain_name || '';
|
||||
|
||||
container.innerHTML = `
|
||||
<span class="asset-code-title">${serviceName}</span>
|
||||
<span class="service-type-badge">${type}</span>
|
||||
<span class="asset-type-label">${domainName}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderHistory(assetId: string) {
|
||||
const container = document.getElementById('domain-history-list');
|
||||
if (!container) return;
|
||||
|
||||
const logs = (state.masterData.logs || []).filter(l => l.asset_id === assetId);
|
||||
if (logs.length === 0) {
|
||||
container.innerHTML = '<div style="color:var(--mute); padding:1rem; text-align:center;">이력이 없습니다.</div>';
|
||||
} else {
|
||||
container.innerHTML = logs.map(l => `<div class="history-item"><div class="history-date">${l.log_date || ''}</div><div class="history-user">${l.log_user || '시스템'}</div><div class="history-details">${l.details}</div></div>`).join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const domainModal = new DomainAssetModal();
|
||||
export function initDomainModal(onSave: () => void, closeModals: () => void) { domainModal.init(onSave, closeModals); }
|
||||
export function openDomainModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') { domainModal.open(asset, mode); }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,295 +1,295 @@
|
||||
import { state, saveJobSpec, deleteJobSpec } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { setFieldValue } from './ModalUtils';
|
||||
import { UI_TEXT } from '../../core/schema';
|
||||
import { calculatePcScoreDeductive } from '../../core/utils';
|
||||
|
||||
class JobSpecModal extends BaseModal {
|
||||
constructor() {
|
||||
super('job-spec', '직무별 기준 사양');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="job-spec-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content narrow">
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<h2 id="job-spec-modal-title" class="modal-title">\${this.title}</h2>
|
||||
<div id="job-spec-header-identity" class="header-identity"></div>
|
||||
</div>
|
||||
<button id="btn-close-job-spec-modal" class="btn-icon" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="job-spec-asset-form" class="grid-form vertical-form">
|
||||
<input type="hidden" id="job-spec-id" name="id" />
|
||||
|
||||
<div class="form-group">
|
||||
<label>직무명</label>
|
||||
<input type="text" id="job-spec-job-name" name="job_name" placeholder="예: BIM 모델러, 개발자, 엔지니어" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group relative">
|
||||
<label>권장 CPU 사양</label>
|
||||
<input type="text" id="job-spec-cpu-standard" name="cpu_standard" placeholder="CPU 검색..." required autocomplete="off" />
|
||||
<div id="job-spec-cpu-autocomplete" class="autocomplete-list hidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group relative">
|
||||
<label>권장 RAM 사양</label>
|
||||
<input type="text" id="job-spec-ram-standard" name="ram_standard" placeholder="RAM 검색..." required autocomplete="off" />
|
||||
<div id="job-spec-ram-autocomplete" class="autocomplete-list hidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group relative">
|
||||
<label>권장 GPU 사양</label>
|
||||
<input type="text" id="job-spec-gpu-standard" name="gpu_standard" placeholder="GPU 검색..." required autocomplete="off" />
|
||||
<div id="job-spec-gpu-autocomplete" class="autocomplete-list hidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>성능 기준 점수 (이상, 자동 계산됨)</label>
|
||||
<input type="number" id="job-spec-min-score" name="min_score" placeholder="자동 계산 대기..." required readonly />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>비고 (메모)</label>
|
||||
<textarea id="job-spec-remarks" name="remarks" placeholder="기타 필요 사양 및 안내 사항" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-job-spec-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-revert-job-spec-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-cancel-job-spec-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-job-spec-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
border: 1px solid var(--border-color, #E2E8F0);
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.autocomplete-item {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.autocomplete-item:hover {
|
||||
background-color: #F1F5F9;
|
||||
color: #1E5149;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const saveBtn = document.getElementById('btn-save-job-spec-asset')!;
|
||||
const revertBtn = document.getElementById('btn-revert-job-spec-edit')!;
|
||||
const deleteBtn = document.getElementById('btn-delete-job-spec-asset')!;
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset) return;
|
||||
if (!this.isEditMode) {
|
||||
this.setEditLockMode('edit');
|
||||
this.isEditMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const jobName = (document.getElementById('job-spec-job-name') as HTMLInputElement).value.trim();
|
||||
const cpuStd = (document.getElementById('job-spec-cpu-standard') as HTMLInputElement).value.trim();
|
||||
const ramStd = (document.getElementById('job-spec-ram-standard') as HTMLInputElement).value.trim();
|
||||
const gpuStd = (document.getElementById('job-spec-gpu-standard') as HTMLInputElement).value.trim();
|
||||
const minScoreStr = (document.getElementById('job-spec-min-score') as HTMLInputElement).value;
|
||||
const remarks = (document.getElementById('job-spec-remarks') as HTMLTextAreaElement).value.trim();
|
||||
|
||||
if (!jobName) {
|
||||
alert('직무명을 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = {
|
||||
id: this.currentAsset.id || null,
|
||||
job_name: jobName,
|
||||
cpu_standard: cpuStd,
|
||||
ram_standard: ramStd,
|
||||
gpu_standard: gpuStd,
|
||||
min_score: minScoreStr !== '' ? parseInt(minScoreStr, 10) : 0,
|
||||
remarks: remarks
|
||||
};
|
||||
|
||||
if (await saveJobSpec(updated)) {
|
||||
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
revertBtn.addEventListener('click', () => {
|
||||
this.setEditLockMode('view');
|
||||
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset || !this.currentAsset.id) return;
|
||||
if (!confirm('정말로 이 직무별 기준 사양을 삭제하시겠습니까?')) return;
|
||||
|
||||
if (await deleteJobSpec(this.currentAsset.id)) {
|
||||
alert('성공적으로 삭제되었습니다.');
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
// 자동완성 바인딩
|
||||
this.bindAutocomplete('job-spec-cpu-standard', 'job-spec-cpu-autocomplete', 'CPU');
|
||||
this.bindAutocomplete('job-spec-ram-standard', 'job-spec-ram-autocomplete', 'RAM');
|
||||
this.bindAutocomplete('job-spec-gpu-standard', 'job-spec-gpu-autocomplete', 'GPU');
|
||||
|
||||
// 실시간 점수 계산 이벤트 바인딩
|
||||
const inputs = ['job-spec-cpu-standard', 'job-spec-ram-standard', 'job-spec-gpu-standard'];
|
||||
inputs.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
el?.addEventListener('input', () => this.updateMinScore());
|
||||
el?.addEventListener('change', () => this.updateMinScore());
|
||||
});
|
||||
}
|
||||
|
||||
private bindAutocomplete(inputId: string, autocompleteId: string, category: string) {
|
||||
const input = document.getElementById(inputId) as HTMLInputElement;
|
||||
const list = document.getElementById(autocompleteId) as HTMLDivElement;
|
||||
if (!input || !list) return;
|
||||
|
||||
const showList = (filterText: string = '') => {
|
||||
if (!this.isEditMode) return;
|
||||
const items = (state.masterData.partsMaster || []).filter((c: any) => c.category === category);
|
||||
const filtered = filterText
|
||||
? items.filter((c: any) => c.component_name.toLowerCase().includes(filterText.toLowerCase()))
|
||||
: items;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
list.innerHTML = '<div class="autocomplete-item" style="color: #94a3b8; cursor: default;">검색 결과 없음</div>';
|
||||
} else {
|
||||
list.innerHTML = filtered.map((c: any) => `<div class="autocomplete-item" data-val="${c.component_name}">${c.component_name}</div>`).join('');
|
||||
}
|
||||
list.classList.remove('hidden');
|
||||
};
|
||||
|
||||
input.addEventListener('focus', () => {
|
||||
showList(input.value);
|
||||
});
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
showList(input.value);
|
||||
});
|
||||
|
||||
list.addEventListener('mousedown', (e) => {
|
||||
const item = (e.target as HTMLElement).closest('.autocomplete-item');
|
||||
if (item && item.getAttribute('data-val')) {
|
||||
input.value = item.getAttribute('data-val') || '';
|
||||
list.classList.add('hidden');
|
||||
this.updateMinScore();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mousedown', (e) => {
|
||||
if (e.target !== input && !list.contains(e.target as Node)) {
|
||||
list.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateMinScore(): void {
|
||||
const cpu = (document.getElementById('job-spec-cpu-standard') as HTMLInputElement)?.value || '';
|
||||
const ram = (document.getElementById('job-spec-ram-standard') as HTMLInputElement)?.value || '';
|
||||
const gpu = (document.getElementById('job-spec-gpu-standard') as HTMLInputElement)?.value || '';
|
||||
|
||||
const score = calculatePcScoreDeductive(cpu, ram, gpu, '');
|
||||
|
||||
const minScoreEl = document.getElementById('job-spec-min-score') as HTMLInputElement;
|
||||
if (minScoreEl) {
|
||||
minScoreEl.value = score.toString();
|
||||
}
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
setFieldValue('job-spec-id', asset.id || '');
|
||||
setFieldValue('job-spec-job-name', asset.job_name || '');
|
||||
setFieldValue('job-spec-cpu-standard', asset.cpu_standard || '');
|
||||
setFieldValue('job-spec-ram-standard', asset.ram_standard || '');
|
||||
setFieldValue('job-spec-gpu-standard', asset.gpu_standard || '');
|
||||
setFieldValue('job-spec-min-score', asset.min_score !== undefined ? asset.min_score.toString() : '100');
|
||||
setFieldValue('job-spec-remarks', asset.remarks || '');
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
protected onAfterOpen(asset: any, mode: string): void {
|
||||
const titleEl = document.getElementById('job-spec-modal-title');
|
||||
|
||||
if (titleEl) {
|
||||
if (mode === 'add') {
|
||||
titleEl.textContent = '신규 직무별 기준 사양 등록';
|
||||
} else {
|
||||
titleEl.textContent = '직무별 기준 사양 상세 편집';
|
||||
}
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('btn-delete-job-spec-asset')!;
|
||||
const saveBtn = document.getElementById('btn-save-job-spec-asset')!;
|
||||
|
||||
deleteBtn.style.display = (mode === 'add') ? 'none' : 'block';
|
||||
|
||||
if (mode === 'add' || mode === 'edit') {
|
||||
saveBtn.textContent = (mode === 'add') ? '등록' : '저장';
|
||||
saveBtn.style.display = 'block';
|
||||
} else {
|
||||
saveBtn.textContent = '수정';
|
||||
saveBtn.style.display = 'block';
|
||||
}
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
private updateHeaderIdentity(asset: any) {
|
||||
const container = document.getElementById('job-spec-header-identity');
|
||||
if (!container) return;
|
||||
|
||||
if (this.currentMode === 'add') {
|
||||
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const jobName = asset.job_name || '';
|
||||
const minScore = asset.min_score || 0;
|
||||
|
||||
container.innerHTML = `
|
||||
<span class="asset-code-title">${jobName}</span>
|
||||
<span class="service-type-badge">${minScore}점 기준</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const jobSpecModal = new JobSpecModal();
|
||||
|
||||
export function initJobSpecModal(onSave: () => void, closeModals: () => void) {
|
||||
jobSpecModal.init(onSave, closeModals);
|
||||
}
|
||||
|
||||
export function openJobSpecModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') {
|
||||
jobSpecModal.open(asset, mode);
|
||||
}
|
||||
import { state, saveJobSpec, deleteJobSpec } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { setFieldValue } from './ModalUtils';
|
||||
import { UI_TEXT } from '../../core/schema';
|
||||
import { calculatePcScoreDeductive } from '../../core/utils';
|
||||
|
||||
class JobSpecModal extends BaseModal {
|
||||
constructor() {
|
||||
super('job-spec', '직무별 기준 사양');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="job-spec-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content narrow">
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<h2 id="job-spec-modal-title" class="modal-title">\${this.title}</h2>
|
||||
<div id="job-spec-header-identity" class="header-identity"></div>
|
||||
</div>
|
||||
<button id="btn-close-job-spec-modal" class="btn-icon" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="job-spec-asset-form" class="grid-form vertical-form">
|
||||
<input type="hidden" id="job-spec-id" name="id" />
|
||||
|
||||
<div class="form-group">
|
||||
<label>직무명</label>
|
||||
<input type="text" id="job-spec-job-name" name="job_name" placeholder="예: BIM 모델러, 개발자, 엔지니어" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group relative">
|
||||
<label>권장 CPU 사양</label>
|
||||
<input type="text" id="job-spec-cpu-standard" name="cpu_standard" placeholder="CPU 검색..." required autocomplete="off" />
|
||||
<div id="job-spec-cpu-autocomplete" class="autocomplete-list hidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group relative">
|
||||
<label>권장 RAM 사양</label>
|
||||
<input type="text" id="job-spec-ram-standard" name="ram_standard" placeholder="RAM 검색..." required autocomplete="off" />
|
||||
<div id="job-spec-ram-autocomplete" class="autocomplete-list hidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group relative">
|
||||
<label>권장 GPU 사양</label>
|
||||
<input type="text" id="job-spec-gpu-standard" name="gpu_standard" placeholder="GPU 검색..." required autocomplete="off" />
|
||||
<div id="job-spec-gpu-autocomplete" class="autocomplete-list hidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>성능 기준 점수 (이상, 자동 계산됨)</label>
|
||||
<input type="number" id="job-spec-min-score" name="min_score" placeholder="자동 계산 대기..." required readonly />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>비고 (메모)</label>
|
||||
<textarea id="job-spec-remarks" name="remarks" placeholder="기타 필요 사양 및 안내 사항" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-job-spec-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-revert-job-spec-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-cancel-job-spec-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-job-spec-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
border: 1px solid var(--border-color, #E2E8F0);
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.autocomplete-item {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.autocomplete-item:hover {
|
||||
background-color: #F1F5F9;
|
||||
color: #1E5149;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const saveBtn = document.getElementById('btn-save-job-spec-asset')!;
|
||||
const revertBtn = document.getElementById('btn-revert-job-spec-edit')!;
|
||||
const deleteBtn = document.getElementById('btn-delete-job-spec-asset')!;
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset) return;
|
||||
if (!this.isEditMode) {
|
||||
this.setEditLockMode('edit');
|
||||
this.isEditMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const jobName = (document.getElementById('job-spec-job-name') as HTMLInputElement).value.trim();
|
||||
const cpuStd = (document.getElementById('job-spec-cpu-standard') as HTMLInputElement).value.trim();
|
||||
const ramStd = (document.getElementById('job-spec-ram-standard') as HTMLInputElement).value.trim();
|
||||
const gpuStd = (document.getElementById('job-spec-gpu-standard') as HTMLInputElement).value.trim();
|
||||
const minScoreStr = (document.getElementById('job-spec-min-score') as HTMLInputElement).value;
|
||||
const remarks = (document.getElementById('job-spec-remarks') as HTMLTextAreaElement).value.trim();
|
||||
|
||||
if (!jobName) {
|
||||
alert('직무명을 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = {
|
||||
id: this.currentAsset.id || null,
|
||||
job_name: jobName,
|
||||
cpu_standard: cpuStd,
|
||||
ram_standard: ramStd,
|
||||
gpu_standard: gpuStd,
|
||||
min_score: minScoreStr !== '' ? parseInt(minScoreStr, 10) : 0,
|
||||
remarks: remarks
|
||||
};
|
||||
|
||||
if (await saveJobSpec(updated)) {
|
||||
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
revertBtn.addEventListener('click', () => {
|
||||
this.setEditLockMode('view');
|
||||
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset || !this.currentAsset.id) return;
|
||||
if (!confirm('정말로 이 직무별 기준 사양을 삭제하시겠습니까?')) return;
|
||||
|
||||
if (await deleteJobSpec(this.currentAsset.id)) {
|
||||
alert('성공적으로 삭제되었습니다.');
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
// 자동완성 바인딩
|
||||
this.bindAutocomplete('job-spec-cpu-standard', 'job-spec-cpu-autocomplete', 'CPU');
|
||||
this.bindAutocomplete('job-spec-ram-standard', 'job-spec-ram-autocomplete', 'RAM');
|
||||
this.bindAutocomplete('job-spec-gpu-standard', 'job-spec-gpu-autocomplete', 'GPU');
|
||||
|
||||
// 실시간 점수 계산 이벤트 바인딩
|
||||
const inputs = ['job-spec-cpu-standard', 'job-spec-ram-standard', 'job-spec-gpu-standard'];
|
||||
inputs.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
el?.addEventListener('input', () => this.updateMinScore());
|
||||
el?.addEventListener('change', () => this.updateMinScore());
|
||||
});
|
||||
}
|
||||
|
||||
private bindAutocomplete(inputId: string, autocompleteId: string, category: string) {
|
||||
const input = document.getElementById(inputId) as HTMLInputElement;
|
||||
const list = document.getElementById(autocompleteId) as HTMLDivElement;
|
||||
if (!input || !list) return;
|
||||
|
||||
const showList = (filterText: string = '') => {
|
||||
if (!this.isEditMode) return;
|
||||
const items = (state.masterData.partsMaster || []).filter((c: any) => c.category === category);
|
||||
const filtered = filterText
|
||||
? items.filter((c: any) => c.component_name.toLowerCase().includes(filterText.toLowerCase()))
|
||||
: items;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
list.innerHTML = '<div class="autocomplete-item" style="color: #94a3b8; cursor: default;">검색 결과 없음</div>';
|
||||
} else {
|
||||
list.innerHTML = filtered.map((c: any) => `<div class="autocomplete-item" data-val="${c.component_name}">${c.component_name}</div>`).join('');
|
||||
}
|
||||
list.classList.remove('hidden');
|
||||
};
|
||||
|
||||
input.addEventListener('focus', () => {
|
||||
showList(input.value);
|
||||
});
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
showList(input.value);
|
||||
});
|
||||
|
||||
list.addEventListener('mousedown', (e) => {
|
||||
const item = (e.target as HTMLElement).closest('.autocomplete-item');
|
||||
if (item && item.getAttribute('data-val')) {
|
||||
input.value = item.getAttribute('data-val') || '';
|
||||
list.classList.add('hidden');
|
||||
this.updateMinScore();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mousedown', (e) => {
|
||||
if (e.target !== input && !list.contains(e.target as Node)) {
|
||||
list.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateMinScore(): void {
|
||||
const cpu = (document.getElementById('job-spec-cpu-standard') as HTMLInputElement)?.value || '';
|
||||
const ram = (document.getElementById('job-spec-ram-standard') as HTMLInputElement)?.value || '';
|
||||
const gpu = (document.getElementById('job-spec-gpu-standard') as HTMLInputElement)?.value || '';
|
||||
|
||||
const score = calculatePcScoreDeductive(cpu, ram, gpu, '');
|
||||
|
||||
const minScoreEl = document.getElementById('job-spec-min-score') as HTMLInputElement;
|
||||
if (minScoreEl) {
|
||||
minScoreEl.value = score.toString();
|
||||
}
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
setFieldValue('job-spec-id', asset.id || '');
|
||||
setFieldValue('job-spec-job-name', asset.job_name || '');
|
||||
setFieldValue('job-spec-cpu-standard', asset.cpu_standard || '');
|
||||
setFieldValue('job-spec-ram-standard', asset.ram_standard || '');
|
||||
setFieldValue('job-spec-gpu-standard', asset.gpu_standard || '');
|
||||
setFieldValue('job-spec-min-score', asset.min_score !== undefined ? asset.min_score.toString() : '100');
|
||||
setFieldValue('job-spec-remarks', asset.remarks || '');
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
protected onAfterOpen(asset: any, mode: string): void {
|
||||
const titleEl = document.getElementById('job-spec-modal-title');
|
||||
|
||||
if (titleEl) {
|
||||
if (mode === 'add') {
|
||||
titleEl.textContent = '신규 직무별 기준 사양 등록';
|
||||
} else {
|
||||
titleEl.textContent = '직무별 기준 사양 상세 편집';
|
||||
}
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('btn-delete-job-spec-asset')!;
|
||||
const saveBtn = document.getElementById('btn-save-job-spec-asset')!;
|
||||
|
||||
deleteBtn.style.display = (mode === 'add') ? 'none' : 'block';
|
||||
|
||||
if (mode === 'add' || mode === 'edit') {
|
||||
saveBtn.textContent = (mode === 'add') ? '등록' : '저장';
|
||||
saveBtn.style.display = 'block';
|
||||
} else {
|
||||
saveBtn.textContent = '수정';
|
||||
saveBtn.style.display = 'block';
|
||||
}
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
private updateHeaderIdentity(asset: any) {
|
||||
const container = document.getElementById('job-spec-header-identity');
|
||||
if (!container) return;
|
||||
|
||||
if (this.currentMode === 'add') {
|
||||
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const jobName = asset.job_name || '';
|
||||
const minScore = asset.min_score || 0;
|
||||
|
||||
container.innerHTML = `
|
||||
<span class="asset-code-title">${jobName}</span>
|
||||
<span class="service-type-badge">${minScore}점 기준</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const jobSpecModal = new JobSpecModal();
|
||||
|
||||
export function initJobSpecModal(onSave: () => void, closeModals: () => void) {
|
||||
jobSpecModal.init(onSave, closeModals);
|
||||
}
|
||||
|
||||
export function openJobSpecModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') {
|
||||
jobSpecModal.open(asset, mode);
|
||||
}
|
||||
|
||||
@@ -1,261 +1,261 @@
|
||||
import { LOCATION_DATA } from './SharedData';
|
||||
|
||||
/**
|
||||
* 모달 조작 및 UI 생성을 위한 공통 유틸리티
|
||||
*/
|
||||
|
||||
// 1. Select 박스의 Option HTML 생성
|
||||
export function generateOptionsHTML(list: string[], defaultValue: string = '', includeSelectHint: boolean = true): string {
|
||||
let html = includeSelectHint ? '<option value="">선택</option>' : '';
|
||||
html += list.map(item => `<option value="${item}" ${item === defaultValue ? 'selected' : ''}>${item}</option>`).join('');
|
||||
return html;
|
||||
}
|
||||
|
||||
// 2. 안전하게 폼 필드 값 설정 (Null 에러 방지)
|
||||
export function setFieldValue(id: string, value: any) {
|
||||
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
if (el) {
|
||||
el.value = value || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 안전하게 폼 필드 값 읽기
|
||||
export function getFieldValue(id: string): string {
|
||||
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
return el ? el.value : '';
|
||||
}
|
||||
|
||||
// 4. 위치 정보 파싱 및 UI 세팅
|
||||
export function parseAndSetLocation(bldg: string, detail: string, bldgId: string, detailId: string, etcGroupId?: string, etcInputId?: string) {
|
||||
const bldgSelect = document.getElementById(bldgId) as HTMLSelectElement;
|
||||
const detailSelect = document.getElementById(detailId) as HTMLSelectElement;
|
||||
const etcGroup = etcGroupId ? document.getElementById(etcGroupId) : null;
|
||||
const etcInput = etcInputId ? document.getElementById(etcInputId) as HTMLInputElement : null;
|
||||
|
||||
if (!bldgSelect || !detailSelect) return;
|
||||
|
||||
// 초기화
|
||||
bldgSelect.value = '';
|
||||
detailSelect.innerHTML = '<option value="">선택</option>';
|
||||
if (etcGroup) etcGroup.style.display = 'none';
|
||||
|
||||
if (!bldg) return;
|
||||
|
||||
if (LOCATION_DATA[bldg]) {
|
||||
bldgSelect.value = bldg;
|
||||
// 상세 목록 갱신
|
||||
detailSelect.innerHTML = generateOptionsHTML(LOCATION_DATA[bldg]);
|
||||
|
||||
if (detail) {
|
||||
detailSelect.value = detail;
|
||||
if (detail === '기타' && etcGroup && etcInput) {
|
||||
etcGroup.style.display = 'flex';
|
||||
// 기타 입력값은 기존 로직 보존을 위해 location_detail을 그대로 쓰거나
|
||||
// 하위 호환성을 위해 남겨둠
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 위치 종속성(Cascade) 이벤트 바인딩
|
||||
export function bindLocationEvents(bldgId: string, detailId: string, etcGroupId: string, etcInputId: string) {
|
||||
const bldgSelect = document.getElementById(bldgId) as HTMLSelectElement;
|
||||
const detailSelect = document.getElementById(detailId) as HTMLSelectElement;
|
||||
const etcGroup = document.getElementById(etcGroupId);
|
||||
const etcInput = document.getElementById(etcInputId) as HTMLInputElement;
|
||||
|
||||
if (!bldgSelect || !detailSelect) return;
|
||||
|
||||
bldgSelect.addEventListener('change', () => {
|
||||
const bldg = bldgSelect.value;
|
||||
detailSelect.innerHTML = generateOptionsHTML(LOCATION_DATA[bldg] || []);
|
||||
if (etcGroup) etcGroup.style.display = 'none';
|
||||
if (etcInput) etcInput.value = '';
|
||||
});
|
||||
|
||||
detailSelect.addEventListener('change', () => {
|
||||
if (etcGroup) {
|
||||
etcGroup.style.display = detailSelect.value === '기타' ? 'flex' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 6. 위치 문자열 조합 (저장용)
|
||||
export function getCombinedLocation(bldgId: string, detailId: string, etcInputId: string): string {
|
||||
const bldg = getFieldValue(bldgId);
|
||||
const detail = getFieldValue(detailId);
|
||||
const etc = getFieldValue(etcInputId);
|
||||
|
||||
let combined = bldg;
|
||||
if (detail) combined += ` ${detail}`;
|
||||
if (detail === '기타' && etc) combined += ` ${etc}`;
|
||||
|
||||
return combined.trim();
|
||||
}
|
||||
|
||||
// 7. 조회/수정 모드 UI 통합 제어
|
||||
export function setEditLock(
|
||||
formId: string,
|
||||
mode: 'view' | 'add' | 'edit',
|
||||
options: {
|
||||
saveBtnId: string,
|
||||
revertBtnId: 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) return;
|
||||
|
||||
const isEdit = (mode === 'add' || mode === 'edit');
|
||||
|
||||
if (isEdit) {
|
||||
// 편집 모드 활성화
|
||||
form.classList.remove('is-view-mode');
|
||||
form.classList.add('is-edit-mode');
|
||||
if (saveBtn) saveBtn.textContent = (mode === 'add' ? '등록' : '저장');
|
||||
if (revertBtn) revertBtn.classList.toggle('hidden', mode === 'add');
|
||||
|
||||
// 모든 필드 활성화
|
||||
const inputs = form.querySelectorAll('input, select, textarea');
|
||||
inputs.forEach(input => {
|
||||
const el = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
// 자산번호 및 ID 필드는 편집 모드에서도 잠금 유지 (disabled는 해제하되 readOnly를 적용하여 폼 데이터 수집 가능하게 함)
|
||||
if (el.name !== 'asset_code' && !el.id.includes('asset-id') && !el.id.includes('id-hidden')) {
|
||||
el.disabled = false;
|
||||
if ('readOnly' in el) (el as HTMLInputElement).readOnly = false;
|
||||
} else {
|
||||
el.disabled = false;
|
||||
if ('readOnly' in el) (el as HTMLInputElement).readOnly = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (generateBtn) generateBtn.style.display = (mode === 'add' ? 'flex' : 'none');
|
||||
if (addLogBtn) addLogBtn.style.display = 'flex';
|
||||
} else {
|
||||
// 조회 모드 (잠금)
|
||||
form.classList.remove('is-edit-mode');
|
||||
form.classList.add('is-view-mode');
|
||||
if (saveBtn) saveBtn.textContent = '수정';
|
||||
if (revertBtn) revertBtn.classList.add('hidden');
|
||||
|
||||
// 모든 필드 잠금
|
||||
const inputs = form.querySelectorAll('input, select, textarea');
|
||||
inputs.forEach(input => {
|
||||
const el = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
el.disabled = true;
|
||||
if ('readOnly' in el) (el as HTMLInputElement).readOnly = true;
|
||||
});
|
||||
|
||||
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-hidden" />
|
||||
${formContent}
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-history-area">
|
||||
<div class="history-header">
|
||||
<h3><i data-lucide="history" class="icon-sm"></i> ${options.historyTitle}</h3>
|
||||
<button type="button" id="btn-add-${idPrefix}-log" class="btn btn-outline btn-sm">
|
||||
내역 추가 <i data-lucide="plus" class="icon-sm"></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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 10. 날짜 자동 마스킹 및 포커스 제어 (Auto-jump)
|
||||
*/
|
||||
export function applyDateMask(el: HTMLInputElement) {
|
||||
if (!el) return;
|
||||
|
||||
el.placeholder = 'YYYY-MM-DD';
|
||||
el.maxLength = 10;
|
||||
|
||||
el.addEventListener('input', (e) => {
|
||||
let value = el.value.replace(/[^0-9]/g, ''); // 숫자만 남김
|
||||
let result = '';
|
||||
|
||||
if (value.length <= 4) {
|
||||
result = value;
|
||||
} else if (value.length <= 6) {
|
||||
result = value.substring(0, 4) + '-' + value.substring(4);
|
||||
} else {
|
||||
result = value.substring(0, 4) + '-' + value.substring(4, 6) + '-' + value.substring(6, 10);
|
||||
}
|
||||
|
||||
el.value = result;
|
||||
});
|
||||
|
||||
// 엔터 키나 입력 완료 시 유효성 검사 (선택 사항)
|
||||
el.addEventListener('blur', () => {
|
||||
const val = el.value;
|
||||
if (val && !/^\d{4}-\d{2}-\d{2}$/.test(val)) {
|
||||
// 형식이 맞지 않으면 경고 효과 등을 줄 수 있음
|
||||
}
|
||||
});
|
||||
}
|
||||
import { LOCATION_DATA } from './SharedData';
|
||||
|
||||
/**
|
||||
* 모달 조작 및 UI 생성을 위한 공통 유틸리티
|
||||
*/
|
||||
|
||||
// 1. Select 박스의 Option HTML 생성
|
||||
export function generateOptionsHTML(list: string[], defaultValue: string = '', includeSelectHint: boolean = true): string {
|
||||
let html = includeSelectHint ? '<option value="">선택</option>' : '';
|
||||
html += list.map(item => `<option value="${item}" ${item === defaultValue ? 'selected' : ''}>${item}</option>`).join('');
|
||||
return html;
|
||||
}
|
||||
|
||||
// 2. 안전하게 폼 필드 값 설정 (Null 에러 방지)
|
||||
export function setFieldValue(id: string, value: any) {
|
||||
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
if (el) {
|
||||
el.value = value || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 안전하게 폼 필드 값 읽기
|
||||
export function getFieldValue(id: string): string {
|
||||
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
return el ? el.value : '';
|
||||
}
|
||||
|
||||
// 4. 위치 정보 파싱 및 UI 세팅
|
||||
export function parseAndSetLocation(bldg: string, detail: string, bldgId: string, detailId: string, etcGroupId?: string, etcInputId?: string) {
|
||||
const bldgSelect = document.getElementById(bldgId) as HTMLSelectElement;
|
||||
const detailSelect = document.getElementById(detailId) as HTMLSelectElement;
|
||||
const etcGroup = etcGroupId ? document.getElementById(etcGroupId) : null;
|
||||
const etcInput = etcInputId ? document.getElementById(etcInputId) as HTMLInputElement : null;
|
||||
|
||||
if (!bldgSelect || !detailSelect) return;
|
||||
|
||||
// 초기화
|
||||
bldgSelect.value = '';
|
||||
detailSelect.innerHTML = '<option value="">선택</option>';
|
||||
if (etcGroup) etcGroup.style.display = 'none';
|
||||
|
||||
if (!bldg) return;
|
||||
|
||||
if (LOCATION_DATA[bldg]) {
|
||||
bldgSelect.value = bldg;
|
||||
// 상세 목록 갱신
|
||||
detailSelect.innerHTML = generateOptionsHTML(LOCATION_DATA[bldg]);
|
||||
|
||||
if (detail) {
|
||||
detailSelect.value = detail;
|
||||
if (detail === '기타' && etcGroup && etcInput) {
|
||||
etcGroup.style.display = 'flex';
|
||||
// 기타 입력값은 기존 로직 보존을 위해 location_detail을 그대로 쓰거나
|
||||
// 하위 호환성을 위해 남겨둠
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 위치 종속성(Cascade) 이벤트 바인딩
|
||||
export function bindLocationEvents(bldgId: string, detailId: string, etcGroupId: string, etcInputId: string) {
|
||||
const bldgSelect = document.getElementById(bldgId) as HTMLSelectElement;
|
||||
const detailSelect = document.getElementById(detailId) as HTMLSelectElement;
|
||||
const etcGroup = document.getElementById(etcGroupId);
|
||||
const etcInput = document.getElementById(etcInputId) as HTMLInputElement;
|
||||
|
||||
if (!bldgSelect || !detailSelect) return;
|
||||
|
||||
bldgSelect.addEventListener('change', () => {
|
||||
const bldg = bldgSelect.value;
|
||||
detailSelect.innerHTML = generateOptionsHTML(LOCATION_DATA[bldg] || []);
|
||||
if (etcGroup) etcGroup.style.display = 'none';
|
||||
if (etcInput) etcInput.value = '';
|
||||
});
|
||||
|
||||
detailSelect.addEventListener('change', () => {
|
||||
if (etcGroup) {
|
||||
etcGroup.style.display = detailSelect.value === '기타' ? 'flex' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 6. 위치 문자열 조합 (저장용)
|
||||
export function getCombinedLocation(bldgId: string, detailId: string, etcInputId: string): string {
|
||||
const bldg = getFieldValue(bldgId);
|
||||
const detail = getFieldValue(detailId);
|
||||
const etc = getFieldValue(etcInputId);
|
||||
|
||||
let combined = bldg;
|
||||
if (detail) combined += ` ${detail}`;
|
||||
if (detail === '기타' && etc) combined += ` ${etc}`;
|
||||
|
||||
return combined.trim();
|
||||
}
|
||||
|
||||
// 7. 조회/수정 모드 UI 통합 제어
|
||||
export function setEditLock(
|
||||
formId: string,
|
||||
mode: 'view' | 'add' | 'edit',
|
||||
options: {
|
||||
saveBtnId: string,
|
||||
revertBtnId: 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) return;
|
||||
|
||||
const isEdit = (mode === 'add' || mode === 'edit');
|
||||
|
||||
if (isEdit) {
|
||||
// 편집 모드 활성화
|
||||
form.classList.remove('is-view-mode');
|
||||
form.classList.add('is-edit-mode');
|
||||
if (saveBtn) saveBtn.textContent = (mode === 'add' ? '등록' : '저장');
|
||||
if (revertBtn) revertBtn.classList.toggle('hidden', mode === 'add');
|
||||
|
||||
// 모든 필드 활성화
|
||||
const inputs = form.querySelectorAll('input, select, textarea');
|
||||
inputs.forEach(input => {
|
||||
const el = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
// 자산번호 및 ID 필드는 편집 모드에서도 잠금 유지 (disabled는 해제하되 readOnly를 적용하여 폼 데이터 수집 가능하게 함)
|
||||
if (el.name !== 'asset_code' && !el.id.includes('asset-id') && !el.id.includes('id-hidden')) {
|
||||
el.disabled = false;
|
||||
if ('readOnly' in el) (el as HTMLInputElement).readOnly = false;
|
||||
} else {
|
||||
el.disabled = false;
|
||||
if ('readOnly' in el) (el as HTMLInputElement).readOnly = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (generateBtn) generateBtn.style.display = (mode === 'add' ? 'flex' : 'none');
|
||||
if (addLogBtn) addLogBtn.style.display = 'flex';
|
||||
} else {
|
||||
// 조회 모드 (잠금)
|
||||
form.classList.remove('is-edit-mode');
|
||||
form.classList.add('is-view-mode');
|
||||
if (saveBtn) saveBtn.textContent = '수정';
|
||||
if (revertBtn) revertBtn.classList.add('hidden');
|
||||
|
||||
// 모든 필드 잠금
|
||||
const inputs = form.querySelectorAll('input, select, textarea');
|
||||
inputs.forEach(input => {
|
||||
const el = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
el.disabled = true;
|
||||
if ('readOnly' in el) (el as HTMLInputElement).readOnly = true;
|
||||
});
|
||||
|
||||
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-hidden" />
|
||||
${formContent}
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-history-area">
|
||||
<div class="history-header">
|
||||
<h3><i data-lucide="history" class="icon-sm"></i> ${options.historyTitle}</h3>
|
||||
<button type="button" id="btn-add-${idPrefix}-log" class="btn btn-outline btn-sm">
|
||||
내역 추가 <i data-lucide="plus" class="icon-sm"></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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 10. 날짜 자동 마스킹 및 포커스 제어 (Auto-jump)
|
||||
*/
|
||||
export function applyDateMask(el: HTMLInputElement) {
|
||||
if (!el) return;
|
||||
|
||||
el.placeholder = 'YYYY-MM-DD';
|
||||
el.maxLength = 10;
|
||||
|
||||
el.addEventListener('input', (e) => {
|
||||
let value = el.value.replace(/[^0-9]/g, ''); // 숫자만 남김
|
||||
let result = '';
|
||||
|
||||
if (value.length <= 4) {
|
||||
result = value;
|
||||
} else if (value.length <= 6) {
|
||||
result = value.substring(0, 4) + '-' + value.substring(4);
|
||||
} else {
|
||||
result = value.substring(0, 4) + '-' + value.substring(4, 6) + '-' + value.substring(6, 10);
|
||||
}
|
||||
|
||||
el.value = result;
|
||||
});
|
||||
|
||||
// 엔터 키나 입력 완료 시 유효성 검사 (선택 사항)
|
||||
el.addEventListener('blur', () => {
|
||||
const val = el.value;
|
||||
if (val && !/^\d{4}-\d{2}-\d{2}$/.test(val)) {
|
||||
// 형식이 맞지 않으면 경고 효과 등을 줄 수 있음
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,171 +1,171 @@
|
||||
import { state, savePartsMaster, deletePartsMaster } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils';
|
||||
import { createIcons, X, Save, Plus } from 'lucide';
|
||||
import { UI_TEXT } from '../../core/schema';
|
||||
|
||||
class PartsMasterModal extends BaseModal {
|
||||
constructor() {
|
||||
super('parts-master', '부품 표준 정보');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="parts-master-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content narrow">
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<h2 id="parts-master-modal-title" class="modal-title">${this.title}</h2>
|
||||
<div id="parts-master-header-identity" class="header-identity"></div>
|
||||
</div>
|
||||
<button id="btn-close-parts-master-modal" class="btn-icon" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="parts-master-asset-form" class="grid-form vertical-form">
|
||||
<input type="hidden" id="parts-master-id" name="id" />
|
||||
|
||||
<div class="form-group">
|
||||
<label>부품 분류</label>
|
||||
<select id="parts-master-category" name="category">
|
||||
<option value="CPU">CPU</option>
|
||||
<option value="GPU">GPU</option>
|
||||
<option value="RAM">RAM</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>부품 표준 명칭</label>
|
||||
<input type="text" id="parts-master-component-name" name="component_name" placeholder="예: Intel Core i7-14700K" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>성능 등급</label>
|
||||
<input type="text" id="parts-master-score-tier" name="score_tier" placeholder="예: i7 / S / 최적" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>감점 점수 (양수로 입력)</label>
|
||||
<input type="number" id="parts-master-deduction" name="deduction" placeholder="예: 5" required />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-parts-master-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-revert-parts-master-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-cancel-parts-master-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-parts-master-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const saveBtn = document.getElementById('btn-save-parts-master-asset')!;
|
||||
const revertBtn = document.getElementById('btn-revert-parts-master-edit')!;
|
||||
const deleteBtn = document.getElementById('btn-delete-parts-master-asset')!;
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset) return;
|
||||
if (!this.isEditMode) {
|
||||
this.setEditLockMode('edit');
|
||||
this.isEditMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const category = (document.getElementById('parts-master-category') as HTMLSelectElement).value;
|
||||
const compName = (document.getElementById('parts-master-component-name') as HTMLInputElement).value.trim();
|
||||
const tier = (document.getElementById('parts-master-score-tier') as HTMLInputElement).value.trim();
|
||||
const deductStr = (document.getElementById('parts-master-deduction') as HTMLInputElement).value;
|
||||
|
||||
if (!compName || !tier || deductStr === '') {
|
||||
alert('모든 필드를 올바르게 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = {
|
||||
id: this.currentAsset.id || null,
|
||||
category,
|
||||
component_name: compName,
|
||||
score_tier: tier,
|
||||
deduction: parseInt(deductStr, 10)
|
||||
};
|
||||
|
||||
if (await savePartsMaster(updated)) {
|
||||
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
revertBtn.addEventListener('click', () => {
|
||||
this.setEditLockMode('view');
|
||||
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset || !this.currentAsset.id) return;
|
||||
if (!confirm('정말로 이 부품 마스터 정보를 삭제하시겠습니까?\n삭제 시 기존 등록 PC 중 이 부품명을 사용하는 PC의 자동완성 정합성 체크에 영향을 줄 수 있습니다.')) return;
|
||||
|
||||
if (await deletePartsMaster(Number(this.currentAsset.id))) {
|
||||
alert('성공적으로 삭제되었습니다.');
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
createIcons({ icons: { Plus, X, Save } });
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
setFieldValue('parts-master-id', asset.id || '');
|
||||
setFieldValue('parts-master-category', asset.category || 'CPU');
|
||||
setFieldValue('parts-master-component-name', asset.component_name || '');
|
||||
setFieldValue('parts-master-score-tier', asset.score_tier || '');
|
||||
setFieldValue('parts-master-deduction', asset.deduction !== undefined ? asset.deduction.toString() : '0');
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
protected onAfterOpen(asset: any, mode: string): void {
|
||||
const titleEl = document.getElementById('parts-master-modal-title');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = (mode === 'add') ? '신규 부품 마스터 등록' : '부품 마스터 상세 편집';
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('btn-delete-parts-master-asset')!;
|
||||
const saveBtn = document.getElementById('btn-save-parts-master-asset')!;
|
||||
|
||||
deleteBtn.style.display = (mode === 'add') ? 'none' : 'block';
|
||||
|
||||
if (mode === 'add' || mode === 'edit') {
|
||||
saveBtn.textContent = (mode === 'add') ? '등록' : '저장';
|
||||
saveBtn.style.display = 'block';
|
||||
} else {
|
||||
saveBtn.textContent = '수정';
|
||||
saveBtn.style.display = 'block';
|
||||
}
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
private updateHeaderIdentity(asset: any) {
|
||||
const container = document.getElementById('parts-master-header-identity');
|
||||
if (!container) return;
|
||||
|
||||
if (this.currentMode === 'add') {
|
||||
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const cat = asset.category || '';
|
||||
const name = asset.component_name || '';
|
||||
|
||||
container.innerHTML = `
|
||||
<span class="asset-code-title">${name}</span>
|
||||
<span class="service-type-badge">${cat}</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const partsMasterModal = new PartsMasterModal();
|
||||
export function initPartsMasterModal(onSave: () => void, closeModals: () => void) { partsMasterModal.init(onSave, closeModals); }
|
||||
export function openPartsMasterModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') { partsMasterModal.open(asset, mode); }
|
||||
import { state, savePartsMaster, deletePartsMaster } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils';
|
||||
import { createIcons, X, Save, Plus } from 'lucide';
|
||||
import { UI_TEXT } from '../../core/schema';
|
||||
|
||||
class PartsMasterModal extends BaseModal {
|
||||
constructor() {
|
||||
super('parts-master', '부품 표준 정보');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="parts-master-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content narrow">
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<h2 id="parts-master-modal-title" class="modal-title">${this.title}</h2>
|
||||
<div id="parts-master-header-identity" class="header-identity"></div>
|
||||
</div>
|
||||
<button id="btn-close-parts-master-modal" class="btn-icon" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="parts-master-asset-form" class="grid-form vertical-form">
|
||||
<input type="hidden" id="parts-master-id" name="id" />
|
||||
|
||||
<div class="form-group">
|
||||
<label>부품 분류</label>
|
||||
<select id="parts-master-category" name="category">
|
||||
<option value="CPU">CPU</option>
|
||||
<option value="GPU">GPU</option>
|
||||
<option value="RAM">RAM</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>부품 표준 명칭</label>
|
||||
<input type="text" id="parts-master-component-name" name="component_name" placeholder="예: Intel Core i7-14700K" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>성능 등급</label>
|
||||
<input type="text" id="parts-master-score-tier" name="score_tier" placeholder="예: i7 / S / 최적" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>감점 점수 (양수로 입력)</label>
|
||||
<input type="number" id="parts-master-deduction" name="deduction" placeholder="예: 5" required />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-parts-master-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-revert-parts-master-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-cancel-parts-master-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-parts-master-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const saveBtn = document.getElementById('btn-save-parts-master-asset')!;
|
||||
const revertBtn = document.getElementById('btn-revert-parts-master-edit')!;
|
||||
const deleteBtn = document.getElementById('btn-delete-parts-master-asset')!;
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset) return;
|
||||
if (!this.isEditMode) {
|
||||
this.setEditLockMode('edit');
|
||||
this.isEditMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const category = (document.getElementById('parts-master-category') as HTMLSelectElement).value;
|
||||
const compName = (document.getElementById('parts-master-component-name') as HTMLInputElement).value.trim();
|
||||
const tier = (document.getElementById('parts-master-score-tier') as HTMLInputElement).value.trim();
|
||||
const deductStr = (document.getElementById('parts-master-deduction') as HTMLInputElement).value;
|
||||
|
||||
if (!compName || !tier || deductStr === '') {
|
||||
alert('모든 필드를 올바르게 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = {
|
||||
id: this.currentAsset.id || null,
|
||||
category,
|
||||
component_name: compName,
|
||||
score_tier: tier,
|
||||
deduction: parseInt(deductStr, 10)
|
||||
};
|
||||
|
||||
if (await savePartsMaster(updated)) {
|
||||
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
revertBtn.addEventListener('click', () => {
|
||||
this.setEditLockMode('view');
|
||||
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset || !this.currentAsset.id) return;
|
||||
if (!confirm('정말로 이 부품 마스터 정보를 삭제하시겠습니까?\n삭제 시 기존 등록 PC 중 이 부품명을 사용하는 PC의 자동완성 정합성 체크에 영향을 줄 수 있습니다.')) return;
|
||||
|
||||
if (await deletePartsMaster(Number(this.currentAsset.id))) {
|
||||
alert('성공적으로 삭제되었습니다.');
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
createIcons({ icons: { Plus, X, Save } });
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
setFieldValue('parts-master-id', asset.id || '');
|
||||
setFieldValue('parts-master-category', asset.category || 'CPU');
|
||||
setFieldValue('parts-master-component-name', asset.component_name || '');
|
||||
setFieldValue('parts-master-score-tier', asset.score_tier || '');
|
||||
setFieldValue('parts-master-deduction', asset.deduction !== undefined ? asset.deduction.toString() : '0');
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
protected onAfterOpen(asset: any, mode: string): void {
|
||||
const titleEl = document.getElementById('parts-master-modal-title');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = (mode === 'add') ? '신규 부품 마스터 등록' : '부품 마스터 상세 편집';
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('btn-delete-parts-master-asset')!;
|
||||
const saveBtn = document.getElementById('btn-save-parts-master-asset')!;
|
||||
|
||||
deleteBtn.style.display = (mode === 'add') ? 'none' : 'block';
|
||||
|
||||
if (mode === 'add' || mode === 'edit') {
|
||||
saveBtn.textContent = (mode === 'add') ? '등록' : '저장';
|
||||
saveBtn.style.display = 'block';
|
||||
} else {
|
||||
saveBtn.textContent = '수정';
|
||||
saveBtn.style.display = 'block';
|
||||
}
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
private updateHeaderIdentity(asset: any) {
|
||||
const container = document.getElementById('parts-master-header-identity');
|
||||
if (!container) return;
|
||||
|
||||
if (this.currentMode === 'add') {
|
||||
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const cat = asset.category || '';
|
||||
const name = asset.component_name || '';
|
||||
|
||||
container.innerHTML = `
|
||||
<span class="asset-code-title">${name}</span>
|
||||
<span class="service-type-badge">${cat}</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const partsMasterModal = new PartsMasterModal();
|
||||
export function initPartsMasterModal(onSave: () => void, closeModals: () => void) { partsMasterModal.init(onSave, closeModals); }
|
||||
export function openPartsMasterModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') { partsMasterModal.open(asset, mode); }
|
||||
|
||||
@@ -1,398 +1,398 @@
|
||||
import { state, saveAsset, deleteAsset } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { openSwUserModal } from './SWUserModal';
|
||||
import { createIcons, History, Plus, X, Save, RotateCcw, Calendar, Users } from 'lucide';
|
||||
import { CORP_LIST } from './SharedData';
|
||||
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
|
||||
import { API_BASE_URL } from '../../core/utils';
|
||||
import {
|
||||
generateOptionsHTML,
|
||||
setFieldValue,
|
||||
getFieldValue,
|
||||
applyDateMask
|
||||
} from './ModalUtils';
|
||||
|
||||
class SwAssetModal extends BaseModal {
|
||||
constructor() {
|
||||
super('sw', '소프트웨어 상세 정보');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="sw-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<h2 id="sw-modal-title" class="modal-title">${this.title}</h2>
|
||||
<div id="sw-header-identity" class="header-identity"></div>
|
||||
</div>
|
||||
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기">×</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" name="id" />
|
||||
|
||||
<div class="form-section-title">기본 정보 (Identity)</div>
|
||||
<div class="form-group">
|
||||
<label>자산 유형</label>
|
||||
<select id="sw-asset-type" name="asset_type" required>
|
||||
<option value="내부SW">내부SW</option>
|
||||
<option value="외부SW">외부SW</option>
|
||||
<option value="클라우드">클라우드</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.SW_FIELD.ui}</label>
|
||||
<select id="sw-분야" name="sw_field" required>
|
||||
<option value="업무공통">업무공통</option>
|
||||
<option value="개발S/W">개발S/W</option>
|
||||
<option value="디자인">디자인</option>
|
||||
<option value="설계S/W">설계S/W</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
|
||||
<select id="sw-법인" name="purchase_corp" required>${generateOptionsHTML(CORP_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>${ASSET_SCHEMA.PRODUCT_NAME.ui}</label>
|
||||
<input type="text" id="sw-제품명" name="product_name" required />
|
||||
</div>
|
||||
<div class="form-group cloud-only">
|
||||
<label>${ASSET_SCHEMA.DEV_OBJ.ui} / 플랫폼</label>
|
||||
<input type="text" id="sw-플랫폼명" name="dev_objective" placeholder="개발목적 또는 플랫폼명" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
|
||||
<input type="text" id="sw-부서" name="current_dept" />
|
||||
</div>
|
||||
<div class="form-group sw-user-tracking">
|
||||
<label>${ASSET_SCHEMA.CURRENT_USER.ui}</label>
|
||||
<input type="text" id="sw-user-current" name="user_current" />
|
||||
</div>
|
||||
<div class="form-group sw-user-tracking">
|
||||
<label>${ASSET_SCHEMA.PREV_USER.ui}</label>
|
||||
<input type="text" id="sw-previous-user" name="previous_user" />
|
||||
</div>
|
||||
|
||||
<div class="form-section-title">라이선스 및 계약 정보</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.ASSET_COUNT.ui}</label>
|
||||
<input type="number" id="sw-수량" name="asset_count" min="0" />
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}</label>
|
||||
<input type="text" id="sw-금액" name="purchase_amount" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
||||
</div>
|
||||
|
||||
<div class="form-group cloud-only">
|
||||
<label>${ASSET_SCHEMA.EMAIL_ACCOUNT.ui}</label>
|
||||
<input type="text" id="sw-계정명" name="email_account" />
|
||||
</div>
|
||||
<div class="form-group cloud-only">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_METHOD.ui}</label>
|
||||
<select id="sw-결제수단" name="purchase_method">
|
||||
<option value="">선택안함</option>
|
||||
<option value="법인카드">법인카드</option>
|
||||
<option value="인보이스">인보이스</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-section-title">관리 및 비고</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="sw-구매일" name="purchase_date" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-구매일-picker'); p.value = document.getElementById('sw-구매일').value; p.showPicker();">
|
||||
<i data-lucide="calendar"></i>
|
||||
</button>
|
||||
<input type="date" id="sw-구매일-picker" class="hidden-picker" onchange="document.getElementById('sw-구매일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</label>
|
||||
<input type="text" id="sw-납품업체" name="purchase_vendor" />
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.DEV_MGR.ui}</label>
|
||||
<input type="text" id="sw-개발담당자" name="dev_manager" />
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.PLANNING_MGR.ui}</label>
|
||||
<input type="text" id="sw-기획담당자" name="planning_manager" />
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.SALES_MGR.ui}</label>
|
||||
<input type="text" id="sw-영업담당자" name="sales_manager" />
|
||||
</div>
|
||||
<div class="form-group sw-standard-field" id="sw-expiry-group">
|
||||
<label>${ASSET_SCHEMA.EXPIRED_DATE.ui}</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="sw-만료일" name="expiry_date" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-만료일-picker'); p.value = document.getElementById('sw-만료일').value; p.showPicker();">
|
||||
<i data-lucide="calendar"></i>
|
||||
</button>
|
||||
<input type="date" id="sw-만료일-picker" class="hidden-picker" onchange="document.getElementById('sw-만료일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>${ASSET_SCHEMA.MEMO.ui}</label>
|
||||
<textarea id="sw-비고" name="memo" rows="2"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="sw-user-section" class="user-management-section">
|
||||
<button type="button" id="btn-open-sw-user" class="btn btn-outline btn-sm" title="사용자 관리">
|
||||
<i data-lucide="users"></i> 사용자 관리
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-history-area">
|
||||
<div class="history-header">
|
||||
<h3><i data-lucide="history"></i> 업데이트 내역</h3>
|
||||
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">
|
||||
계약 업데이트 <i data-lucide="rotate-ccw"></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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 계약 업데이트 서브 모달 -->
|
||||
<div id="sw-update-modal" class="modal-overlay hidden sub-modal">
|
||||
<div class="modal-content narrow">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">계약 업데이트 반영</h2>
|
||||
<button id="btn-close-sw-update" class="btn-icon">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="grid-form vertical-form">
|
||||
<div class="form-group">
|
||||
<label>업데이트 일자</label>
|
||||
<input type="date" id="sw-update-date" />
|
||||
</div>
|
||||
<div class="form-group sub-sw-update">
|
||||
<label>새로운 계약 기간</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="sw-update-start" placeholder="YYYY-MM-DD" />
|
||||
<span>~</span>
|
||||
<input type="text" id="sw-update-end" placeholder="YYYY-MM-DD" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>발생 비용</label>
|
||||
<input type="text" id="sw-update-cost" oninput="this.value = this.value.replace(/[^0-9]/g, '') ? Number(this.value.replace(/[^0-9]/g, '')).toLocaleString() : ''" placeholder="ex) 500,000" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>상세 내용 (메모)</label>
|
||||
<input type="text" id="sw-update-note" placeholder="예: 25년도 구독 연장 결제 완료" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div></div>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-sw-update" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-sw-update" class="btn btn-primary">반영하기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.hidden-picker {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const saveBtn = document.getElementById('btn-save-sw-asset')!;
|
||||
const revertBtn = document.getElementById('btn-revert-sw-edit')!;
|
||||
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
|
||||
const typeSelect = document.getElementById('sw-asset-type') as HTMLSelectElement;
|
||||
const userAssignBtn = document.getElementById('btn-open-sw-user')!;
|
||||
const btnOpenUpdate = document.getElementById('btn-open-sw-update')!;
|
||||
|
||||
typeSelect?.addEventListener('change', () => this.applySwTypeUI(typeSelect.value));
|
||||
|
||||
['sw-구매일', 'sw-시작일', 'sw-만료일', 'sw-update-start', 'sw-update-end'].forEach(id => {
|
||||
const el = document.getElementById(id) as HTMLInputElement;
|
||||
if (el) applyDateMask(el);
|
||||
});
|
||||
|
||||
userAssignBtn.addEventListener('click', () => {
|
||||
if (this.currentAsset) openSwUserModal(this.currentAsset);
|
||||
});
|
||||
|
||||
const subModal = document.getElementById('sw-update-modal')!;
|
||||
const closeUpdate = () => subModal.classList.add('hidden');
|
||||
document.getElementById('btn-close-sw-update')?.addEventListener('click', closeUpdate);
|
||||
document.getElementById('btn-cancel-sw-update')?.addEventListener('click', closeUpdate);
|
||||
|
||||
btnOpenUpdate?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!this.isEditMode) { alert('자산을 수정 모드로 변경한 후 업데이트를 진행해주세요.'); return; }
|
||||
subModal.classList.remove('hidden');
|
||||
});
|
||||
|
||||
document.getElementById('btn-save-sw-update')?.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
const date = (document.getElementById('sw-update-date') as HTMLInputElement).value;
|
||||
const start = (document.getElementById('sw-update-start') as HTMLInputElement).value;
|
||||
const end = (document.getElementById('sw-update-end') as HTMLInputElement).value;
|
||||
const cost = (document.getElementById('sw-update-cost') as HTMLInputElement).value;
|
||||
const note = (document.getElementById('sw-update-note') as HTMLInputElement).value;
|
||||
|
||||
if (start) setFieldValue('sw-시작일', start);
|
||||
if (end) setFieldValue('sw-만료일', end);
|
||||
if (cost) setFieldValue('sw-금액', cost);
|
||||
|
||||
const log = { assetId: this.currentAsset.id, date, details: `[계약갱신] ${note} (${start} ~ ${end}, 비용: ${cost})`, user: '관리자' };
|
||||
await fetch(`${API_BASE_URL}/api/asset/history/batch`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify([...state.masterData.logs, log])
|
||||
});
|
||||
|
||||
closeUpdate(); onSave();
|
||||
});
|
||||
|
||||
revertBtn.addEventListener('click', () => {
|
||||
this.setEditLockMode('view');
|
||||
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||
});
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset) return;
|
||||
if (!this.isEditMode) { this.setEditLockMode('edit'); this.isEditMode = true; return; }
|
||||
|
||||
const type = getFieldValue('sw-asset-type');
|
||||
const formData = new FormData(this.formEl!);
|
||||
const updated = { ...this.currentAsset };
|
||||
formData.forEach((value, key) => { updated[key] = value; });
|
||||
|
||||
let categoryKey = (type === '내부SW') ? 'swInternal' : (type === '클라우드' ? 'cloud' : 'swExternal');
|
||||
if (await saveAsset(categoryKey, updated)) { onSave(); this.close(); closeModals(); }
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset || !confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return;
|
||||
const type = this.currentAsset.asset_type || this.currentAsset.type;
|
||||
let categoryKey = (type === '내부SW') ? 'swInternal' : (type === '클라우드' ? 'cloud' : 'swExternal');
|
||||
if (await deleteAsset(categoryKey, this.currentAsset.id)) {
|
||||
alert('성공적으로 삭제되었습니다.'); onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
createIcons({ icons: { History, Plus, Save, Calendar, Users, RotateCcw } });
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
setFieldValue('sw-asset-id', asset.id);
|
||||
setFieldValue('sw-asset-type', asset.asset_type || asset.type);
|
||||
setFieldValue('sw-분야', asset.sw_field || '');
|
||||
setFieldValue('sw-법인', asset.purchase_corp || '');
|
||||
setFieldValue('sw-부서', asset.current_dept || '');
|
||||
setFieldValue('sw-user-current', asset.user_current || '');
|
||||
setFieldValue('sw-previous-user', asset.previous_user || '');
|
||||
setFieldValue('sw-제품명', asset.product_name || '');
|
||||
setFieldValue('sw-수량', asset.asset_count || '');
|
||||
setFieldValue('sw-금액', asset.purchase_amount || '');
|
||||
setFieldValue('sw-구매일', asset.purchase_date || '');
|
||||
setFieldValue('sw-납품업체', asset.purchase_vendor || '');
|
||||
setFieldValue('sw-개발담당자', asset.dev_manager || '');
|
||||
setFieldValue('sw-기획담당자', asset.planning_manager || '');
|
||||
setFieldValue('sw-영업담당자', asset.sales_manager || '');
|
||||
setFieldValue('sw-비고', asset.memo || '');
|
||||
|
||||
if (asset.type === '클라우드' || asset.asset_type === '클라우드') {
|
||||
setFieldValue('sw-플랫폼명', asset.dev_objective || '');
|
||||
setFieldValue('sw-계정명', asset.email_account || '');
|
||||
setFieldValue('sw-결제수단', asset.purchase_method || '');
|
||||
} else {
|
||||
setFieldValue('sw-만료일', asset.expiry_date || '');
|
||||
}
|
||||
|
||||
this.renderHistory(asset.id);
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
protected onAfterOpen(asset: any, mode: string): void {
|
||||
this.applySwTypeUI(asset.asset_type || asset.type);
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
private updateHeaderIdentity(asset: any) {
|
||||
const container = document.getElementById('sw-header-identity');
|
||||
if (!container) return;
|
||||
|
||||
if (this.currentMode === 'add') {
|
||||
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const type = getFieldValue('sw-asset-type') || asset.asset_type || asset.type || '';
|
||||
const name = getFieldValue('sw-제품명') || asset.product_name || '';
|
||||
const corp = getFieldValue('sw-법인') || asset.purchase_corp || '';
|
||||
|
||||
container.innerHTML = `
|
||||
<span class="asset-code-title">${name}</span>
|
||||
<span class="service-type-badge">${corp}</span>
|
||||
<span class="asset-type-label">${type}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
private applySwTypeUI(type: string) {
|
||||
const cloudFields = document.querySelectorAll('.cloud-only');
|
||||
const swFields = document.querySelectorAll('.sw-standard-field');
|
||||
const userSection = document.getElementById('sw-user-section');
|
||||
const expiryGroup = document.getElementById('sw-expiry-group');
|
||||
const userTracking = document.querySelectorAll('.sw-user-tracking');
|
||||
|
||||
if (type === '클라우드') {
|
||||
cloudFields.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
swFields.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||
if (userSection) userSection.style.display = 'none';
|
||||
userTracking.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||
} else {
|
||||
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' || type === '내부SW') {
|
||||
if (expiryGroup) expiryGroup.style.display = 'flex';
|
||||
userTracking.forEach(el => (el as HTMLElement).style.display = (type === '외부SW') ? 'flex' : 'none');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderHistory(swId: string) {
|
||||
const container = document.getElementById('sw-history-list');
|
||||
if (!container) return;
|
||||
const logs = (state.masterData.logs || []).filter(l => l.asset_id === 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.log_date || ''}</div><div class="history-user">${l.log_user || '시스템'}</div><div class="history-details">${l.details}</div></div>`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
export const swModal = new SwAssetModal();
|
||||
export function initSwModal(onSave: () => void, closeModals: () => void) { swModal.init(onSave, closeModals); }
|
||||
export function openSwModal(asset: any, mode: 'view' | 'add' | 'edit' = 'view') { swModal.open(asset, mode); }
|
||||
import { state, saveAsset, deleteAsset } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { openSwUserModal } from './SWUserModal';
|
||||
import { createIcons, History, Plus, X, Save, RotateCcw, Calendar, Users } from 'lucide';
|
||||
import { CORP_LIST } from './SharedData';
|
||||
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
|
||||
import { API_BASE_URL } from '../../core/utils';
|
||||
import {
|
||||
generateOptionsHTML,
|
||||
setFieldValue,
|
||||
getFieldValue,
|
||||
applyDateMask
|
||||
} from './ModalUtils';
|
||||
|
||||
class SwAssetModal extends BaseModal {
|
||||
constructor() {
|
||||
super('sw', '소프트웨어 상세 정보');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="sw-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<h2 id="sw-modal-title" class="modal-title">${this.title}</h2>
|
||||
<div id="sw-header-identity" class="header-identity"></div>
|
||||
</div>
|
||||
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기">×</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" name="id" />
|
||||
|
||||
<div class="form-section-title">기본 정보 (Identity)</div>
|
||||
<div class="form-group">
|
||||
<label>자산 유형</label>
|
||||
<select id="sw-asset-type" name="asset_type" required>
|
||||
<option value="내부SW">내부SW</option>
|
||||
<option value="외부SW">외부SW</option>
|
||||
<option value="클라우드">클라우드</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.SW_FIELD.ui}</label>
|
||||
<select id="sw-분야" name="sw_field" required>
|
||||
<option value="업무공통">업무공통</option>
|
||||
<option value="개발S/W">개발S/W</option>
|
||||
<option value="디자인">디자인</option>
|
||||
<option value="설계S/W">설계S/W</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
|
||||
<select id="sw-법인" name="purchase_corp" required>${generateOptionsHTML(CORP_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>${ASSET_SCHEMA.PRODUCT_NAME.ui}</label>
|
||||
<input type="text" id="sw-제품명" name="product_name" required />
|
||||
</div>
|
||||
<div class="form-group cloud-only">
|
||||
<label>${ASSET_SCHEMA.DEV_OBJ.ui} / 플랫폼</label>
|
||||
<input type="text" id="sw-플랫폼명" name="dev_objective" placeholder="개발목적 또는 플랫폼명" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
|
||||
<input type="text" id="sw-부서" name="current_dept" />
|
||||
</div>
|
||||
<div class="form-group sw-user-tracking">
|
||||
<label>${ASSET_SCHEMA.CURRENT_USER.ui}</label>
|
||||
<input type="text" id="sw-user-current" name="user_current" />
|
||||
</div>
|
||||
<div class="form-group sw-user-tracking">
|
||||
<label>${ASSET_SCHEMA.PREV_USER.ui}</label>
|
||||
<input type="text" id="sw-previous-user" name="previous_user" />
|
||||
</div>
|
||||
|
||||
<div class="form-section-title">라이선스 및 계약 정보</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.ASSET_COUNT.ui}</label>
|
||||
<input type="number" id="sw-수량" name="asset_count" min="0" />
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}</label>
|
||||
<input type="text" id="sw-금액" name="purchase_amount" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
||||
</div>
|
||||
|
||||
<div class="form-group cloud-only">
|
||||
<label>${ASSET_SCHEMA.EMAIL_ACCOUNT.ui}</label>
|
||||
<input type="text" id="sw-계정명" name="email_account" />
|
||||
</div>
|
||||
<div class="form-group cloud-only">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_METHOD.ui}</label>
|
||||
<select id="sw-결제수단" name="purchase_method">
|
||||
<option value="">선택안함</option>
|
||||
<option value="법인카드">법인카드</option>
|
||||
<option value="인보이스">인보이스</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-section-title">관리 및 비고</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="sw-구매일" name="purchase_date" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-구매일-picker'); p.value = document.getElementById('sw-구매일').value; p.showPicker();">
|
||||
<i data-lucide="calendar"></i>
|
||||
</button>
|
||||
<input type="date" id="sw-구매일-picker" class="hidden-picker" onchange="document.getElementById('sw-구매일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</label>
|
||||
<input type="text" id="sw-납품업체" name="purchase_vendor" />
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.DEV_MGR.ui}</label>
|
||||
<input type="text" id="sw-개발담당자" name="dev_manager" />
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.PLANNING_MGR.ui}</label>
|
||||
<input type="text" id="sw-기획담당자" name="planning_manager" />
|
||||
</div>
|
||||
<div class="form-group sw-standard-field">
|
||||
<label>${ASSET_SCHEMA.SALES_MGR.ui}</label>
|
||||
<input type="text" id="sw-영업담당자" name="sales_manager" />
|
||||
</div>
|
||||
<div class="form-group sw-standard-field" id="sw-expiry-group">
|
||||
<label>${ASSET_SCHEMA.EXPIRED_DATE.ui}</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="sw-만료일" name="expiry_date" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-만료일-picker'); p.value = document.getElementById('sw-만료일').value; p.showPicker();">
|
||||
<i data-lucide="calendar"></i>
|
||||
</button>
|
||||
<input type="date" id="sw-만료일-picker" class="hidden-picker" onchange="document.getElementById('sw-만료일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>${ASSET_SCHEMA.MEMO.ui}</label>
|
||||
<textarea id="sw-비고" name="memo" rows="2"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="sw-user-section" class="user-management-section">
|
||||
<button type="button" id="btn-open-sw-user" class="btn btn-outline btn-sm" title="사용자 관리">
|
||||
<i data-lucide="users"></i> 사용자 관리
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-history-area">
|
||||
<div class="history-header">
|
||||
<h3><i data-lucide="history"></i> 업데이트 내역</h3>
|
||||
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">
|
||||
계약 업데이트 <i data-lucide="rotate-ccw"></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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 계약 업데이트 서브 모달 -->
|
||||
<div id="sw-update-modal" class="modal-overlay hidden sub-modal">
|
||||
<div class="modal-content narrow">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">계약 업데이트 반영</h2>
|
||||
<button id="btn-close-sw-update" class="btn-icon">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="grid-form vertical-form">
|
||||
<div class="form-group">
|
||||
<label>업데이트 일자</label>
|
||||
<input type="date" id="sw-update-date" />
|
||||
</div>
|
||||
<div class="form-group sub-sw-update">
|
||||
<label>새로운 계약 기간</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="sw-update-start" placeholder="YYYY-MM-DD" />
|
||||
<span>~</span>
|
||||
<input type="text" id="sw-update-end" placeholder="YYYY-MM-DD" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>발생 비용</label>
|
||||
<input type="text" id="sw-update-cost" oninput="this.value = this.value.replace(/[^0-9]/g, '') ? Number(this.value.replace(/[^0-9]/g, '')).toLocaleString() : ''" placeholder="ex) 500,000" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>상세 내용 (메모)</label>
|
||||
<input type="text" id="sw-update-note" placeholder="예: 25년도 구독 연장 결제 완료" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div></div>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-cancel-sw-update" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-sw-update" class="btn btn-primary">반영하기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.hidden-picker {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const saveBtn = document.getElementById('btn-save-sw-asset')!;
|
||||
const revertBtn = document.getElementById('btn-revert-sw-edit')!;
|
||||
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
|
||||
const typeSelect = document.getElementById('sw-asset-type') as HTMLSelectElement;
|
||||
const userAssignBtn = document.getElementById('btn-open-sw-user')!;
|
||||
const btnOpenUpdate = document.getElementById('btn-open-sw-update')!;
|
||||
|
||||
typeSelect?.addEventListener('change', () => this.applySwTypeUI(typeSelect.value));
|
||||
|
||||
['sw-구매일', 'sw-시작일', 'sw-만료일', 'sw-update-start', 'sw-update-end'].forEach(id => {
|
||||
const el = document.getElementById(id) as HTMLInputElement;
|
||||
if (el) applyDateMask(el);
|
||||
});
|
||||
|
||||
userAssignBtn.addEventListener('click', () => {
|
||||
if (this.currentAsset) openSwUserModal(this.currentAsset);
|
||||
});
|
||||
|
||||
const subModal = document.getElementById('sw-update-modal')!;
|
||||
const closeUpdate = () => subModal.classList.add('hidden');
|
||||
document.getElementById('btn-close-sw-update')?.addEventListener('click', closeUpdate);
|
||||
document.getElementById('btn-cancel-sw-update')?.addEventListener('click', closeUpdate);
|
||||
|
||||
btnOpenUpdate?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!this.isEditMode) { alert('자산을 수정 모드로 변경한 후 업데이트를 진행해주세요.'); return; }
|
||||
subModal.classList.remove('hidden');
|
||||
});
|
||||
|
||||
document.getElementById('btn-save-sw-update')?.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
const date = (document.getElementById('sw-update-date') as HTMLInputElement).value;
|
||||
const start = (document.getElementById('sw-update-start') as HTMLInputElement).value;
|
||||
const end = (document.getElementById('sw-update-end') as HTMLInputElement).value;
|
||||
const cost = (document.getElementById('sw-update-cost') as HTMLInputElement).value;
|
||||
const note = (document.getElementById('sw-update-note') as HTMLInputElement).value;
|
||||
|
||||
if (start) setFieldValue('sw-시작일', start);
|
||||
if (end) setFieldValue('sw-만료일', end);
|
||||
if (cost) setFieldValue('sw-금액', cost);
|
||||
|
||||
const log = { assetId: this.currentAsset.id, date, details: `[계약갱신] ${note} (${start} ~ ${end}, 비용: ${cost})`, user: '관리자' };
|
||||
await fetch(`${API_BASE_URL}/api/asset/history/batch`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify([...state.masterData.logs, log])
|
||||
});
|
||||
|
||||
closeUpdate(); onSave();
|
||||
});
|
||||
|
||||
revertBtn.addEventListener('click', () => {
|
||||
this.setEditLockMode('view');
|
||||
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||
});
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset) return;
|
||||
if (!this.isEditMode) { this.setEditLockMode('edit'); this.isEditMode = true; return; }
|
||||
|
||||
const type = getFieldValue('sw-asset-type');
|
||||
const formData = new FormData(this.formEl!);
|
||||
const updated = { ...this.currentAsset };
|
||||
formData.forEach((value, key) => { updated[key] = value; });
|
||||
|
||||
let categoryKey = (type === '내부SW') ? 'swInternal' : (type === '클라우드' ? 'cloud' : 'swExternal');
|
||||
if (await saveAsset(categoryKey, updated)) { onSave(); this.close(); closeModals(); }
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset || !confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return;
|
||||
const type = this.currentAsset.asset_type || this.currentAsset.type;
|
||||
let categoryKey = (type === '내부SW') ? 'swInternal' : (type === '클라우드' ? 'cloud' : 'swExternal');
|
||||
if (await deleteAsset(categoryKey, this.currentAsset.id)) {
|
||||
alert('성공적으로 삭제되었습니다.'); onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
createIcons({ icons: { History, Plus, Save, Calendar, Users, RotateCcw } });
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
setFieldValue('sw-asset-id', asset.id);
|
||||
setFieldValue('sw-asset-type', asset.asset_type || asset.type);
|
||||
setFieldValue('sw-분야', asset.sw_field || '');
|
||||
setFieldValue('sw-법인', asset.purchase_corp || '');
|
||||
setFieldValue('sw-부서', asset.current_dept || '');
|
||||
setFieldValue('sw-user-current', asset.user_current || '');
|
||||
setFieldValue('sw-previous-user', asset.previous_user || '');
|
||||
setFieldValue('sw-제품명', asset.product_name || '');
|
||||
setFieldValue('sw-수량', asset.asset_count || '');
|
||||
setFieldValue('sw-금액', asset.purchase_amount || '');
|
||||
setFieldValue('sw-구매일', asset.purchase_date || '');
|
||||
setFieldValue('sw-납품업체', asset.purchase_vendor || '');
|
||||
setFieldValue('sw-개발담당자', asset.dev_manager || '');
|
||||
setFieldValue('sw-기획담당자', asset.planning_manager || '');
|
||||
setFieldValue('sw-영업담당자', asset.sales_manager || '');
|
||||
setFieldValue('sw-비고', asset.memo || '');
|
||||
|
||||
if (asset.type === '클라우드' || asset.asset_type === '클라우드') {
|
||||
setFieldValue('sw-플랫폼명', asset.dev_objective || '');
|
||||
setFieldValue('sw-계정명', asset.email_account || '');
|
||||
setFieldValue('sw-결제수단', asset.purchase_method || '');
|
||||
} else {
|
||||
setFieldValue('sw-만료일', asset.expiry_date || '');
|
||||
}
|
||||
|
||||
this.renderHistory(asset.id);
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
protected onAfterOpen(asset: any, mode: string): void {
|
||||
this.applySwTypeUI(asset.asset_type || asset.type);
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
private updateHeaderIdentity(asset: any) {
|
||||
const container = document.getElementById('sw-header-identity');
|
||||
if (!container) return;
|
||||
|
||||
if (this.currentMode === 'add') {
|
||||
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const type = getFieldValue('sw-asset-type') || asset.asset_type || asset.type || '';
|
||||
const name = getFieldValue('sw-제품명') || asset.product_name || '';
|
||||
const corp = getFieldValue('sw-법인') || asset.purchase_corp || '';
|
||||
|
||||
container.innerHTML = `
|
||||
<span class="asset-code-title">${name}</span>
|
||||
<span class="service-type-badge">${corp}</span>
|
||||
<span class="asset-type-label">${type}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
private applySwTypeUI(type: string) {
|
||||
const cloudFields = document.querySelectorAll('.cloud-only');
|
||||
const swFields = document.querySelectorAll('.sw-standard-field');
|
||||
const userSection = document.getElementById('sw-user-section');
|
||||
const expiryGroup = document.getElementById('sw-expiry-group');
|
||||
const userTracking = document.querySelectorAll('.sw-user-tracking');
|
||||
|
||||
if (type === '클라우드') {
|
||||
cloudFields.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||
swFields.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||
if (userSection) userSection.style.display = 'none';
|
||||
userTracking.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||
} else {
|
||||
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' || type === '내부SW') {
|
||||
if (expiryGroup) expiryGroup.style.display = 'flex';
|
||||
userTracking.forEach(el => (el as HTMLElement).style.display = (type === '외부SW') ? 'flex' : 'none');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderHistory(swId: string) {
|
||||
const container = document.getElementById('sw-history-list');
|
||||
if (!container) return;
|
||||
const logs = (state.masterData.logs || []).filter(l => l.asset_id === 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.log_date || ''}</div><div class="history-user">${l.log_user || '시스템'}</div><div class="history-details">${l.details}</div></div>`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
export const swModal = new SwAssetModal();
|
||||
export function initSwModal(onSave: () => void, closeModals: () => void) { swModal.init(onSave, closeModals); }
|
||||
export function openSwModal(asset: any, mode: 'view' | 'add' | 'edit' = 'view') { swModal.open(asset, mode); }
|
||||
|
||||
@@ -1,270 +1,270 @@
|
||||
import { state } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { createIcons, Edit2, X, Paperclip, Calendar, Plus } from 'lucide';
|
||||
import { ORG_LIST } from './SharedData';
|
||||
import { generateOptionsHTML, setFieldValue, getFieldValue, applyDateMask } from './ModalUtils';
|
||||
|
||||
class SwUserModal extends BaseModal {
|
||||
private tempSwUsers: any[] = [];
|
||||
|
||||
constructor() {
|
||||
super('sw-user', '소프트웨어 사용자 관리');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="sw-user-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<h2 id="sw-user-title" class="modal-title">${this.title}</h2>
|
||||
<button id="btn-close-sw-user-modal" class="btn-icon" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="sw-info-summary" id="sw-user-sw-info"></div>
|
||||
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="detail-section-title mb-0">할당된 사용자 목록</h3>
|
||||
<button type="button" id="btn-open-add-user" class="btn btn-primary btn-sm"><i data-lucide="plus" class="icon-sm"></i> 사용자 추가</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>조직</th>
|
||||
<th>부서</th>
|
||||
<th>직위</th>
|
||||
<th>이름</th>
|
||||
<th class="text-center">사용기간</th>
|
||||
<th class="text-center">신청서</th>
|
||||
<th class="text-center">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sw-user-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- 더미 폼 (BaseModal 필수 요건 충족용) -->
|
||||
<form id="sw-user-asset-form" class="hidden"></form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-cancel-sw-user" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-sw-user" class="btn btn-primary">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사용자 추가/수정 서브 모달 -->
|
||||
<div id="sw-user-edit-modal" class="modal-overlay hidden sub-modal">
|
||||
<div class="modal-content narrow">
|
||||
<div class="modal-header">
|
||||
<h3 id="sw-user-edit-title" class="modal-title">사용자 정보</h3>
|
||||
<button id="btn-close-user-edit" class="btn-icon">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="sw-user-edit-form" class="grid-form vertical-form">
|
||||
<input type="hidden" id="edit-user-index" value="-1" />
|
||||
<div class="form-group">
|
||||
<label>조직</label>
|
||||
<select id="new-user-조직">${generateOptionsHTML(ORG_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>부서</label>
|
||||
<input type="text" id="new-user-부서" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>직위</label>
|
||||
<input type="text" id="new-user-직위" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>이름</label>
|
||||
<input type="text" id="new-user-이름" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>사용 시작일</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="new-user-시작일" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-시작일-picker'); p.value = document.getElementById('new-user-시작일').value; p.showPicker();">
|
||||
<i data-lucide="calendar" class="icon-sm"></i>
|
||||
</button>
|
||||
<input type="date" id="new-user-시작일-picker" class="hidden-picker" onchange="document.getElementById('new-user-시작일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>사용 종료일</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="new-user-종료일" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-종료일-picker'); p.value = document.getElementById('new-user-종료일').value; p.showPicker();">
|
||||
<i data-lucide="calendar" class="icon-sm"></i>
|
||||
</button>
|
||||
<input type="date" id="new-user-종료일-picker" class="hidden-picker" onchange="document.getElementById('new-user-종료일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>신청서 (증빙)</label>
|
||||
<input type="file" id="new-user-신청서" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-close-user-sub" class="btn btn-outline">취소</button>
|
||||
<button id="btn-confirm-user-edit" class="btn btn-primary">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.hidden-picker {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const mainSaveBtn = document.getElementById('btn-save-sw-user')!;
|
||||
const addUserBtn = document.getElementById('btn-open-add-user')!;
|
||||
const confirmUserBtn = document.getElementById('btn-confirm-user-edit')!;
|
||||
|
||||
['new-user-시작일', 'new-user-종료일'].forEach(id => {
|
||||
const el = document.getElementById(id) as HTMLInputElement;
|
||||
if (el) applyDateMask(el);
|
||||
});
|
||||
|
||||
addUserBtn.addEventListener('click', () => this.openUserEditSubModal());
|
||||
confirmUserBtn.addEventListener('click', () => this.saveUserDataToList());
|
||||
|
||||
mainSaveBtn.addEventListener('click', () => {
|
||||
if (!this.currentAsset) return;
|
||||
const existingIdx = state.masterData.swUsers.findIndex(u => u.sw_id === this.currentAsset!.id);
|
||||
const newMapping = {
|
||||
sw_id: this.currentAsset!.id,
|
||||
userData: this.tempSwUsers.map(u => [u.조직, u.부서, u.직위, u.이름, u.사용기간, u.신청서명])
|
||||
};
|
||||
if (existingIdx > -1) state.masterData.swUsers[existingIdx] = newMapping as any;
|
||||
else state.masterData.swUsers.push(newMapping as any);
|
||||
|
||||
onSave(); this.close(); closeModals();
|
||||
});
|
||||
|
||||
document.getElementById('btn-close-sw-user-modal')?.addEventListener('click', () => this.close());
|
||||
document.getElementById('btn-cancel-sw-user')?.addEventListener('click', () => this.close());
|
||||
|
||||
const subModal = document.getElementById('sw-user-edit-modal')!;
|
||||
const closeSub = () => subModal.classList.add('hidden');
|
||||
document.getElementById('btn-close-user-edit')?.addEventListener('click', closeSub);
|
||||
document.getElementById('btn-close-user-sub')?.addEventListener('click', closeSub);
|
||||
|
||||
createIcons({ icons: { X, Plus, Calendar, Edit2, Paperclip } });
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
const swInfo = document.getElementById('sw-user-sw-info')!;
|
||||
swInfo.innerHTML = `
|
||||
<div class="sw-info-header border-b border-hairline pb-4 mb-6">
|
||||
<div class="detail-label-sm">${asset.purchase_corp || asset.법인 || ''}</div>
|
||||
<div class="asset-code-title">${asset.product_name || asset.제품명 || ''}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const existingMapping = state.masterData.swUsers.find(u => u.sw_id === asset.id);
|
||||
this.tempSwUsers = existingMapping ? (existingMapping.userData || []).map((u: any) => ({
|
||||
조직: u[0], 부서: u[1], 직위: u[2], 이름: u[3], 사용기간: u[4], 신청서명: u[5]
|
||||
})) : [];
|
||||
|
||||
this.renderUserList();
|
||||
}
|
||||
|
||||
protected onAfterOpen(): void {}
|
||||
|
||||
private renderUserList() {
|
||||
const tbody = document.getElementById('sw-user-table-body')!;
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = '';
|
||||
if (this.tempSwUsers.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="empty-cell text-center p-8">할당된 사용자가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.tempSwUsers.forEach((user, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${user.조직 || ''}</td>
|
||||
<td>${user.부서 || ''}</td>
|
||||
<td>${user.직위 || ''}</td>
|
||||
<td>${user.이름 || ''}</td>
|
||||
<td class="text-center">${user.사용기간 || ''}</td>
|
||||
<td class="text-center">${user.신청서명 ? '<i data-lucide="paperclip" class="text-primary icon-sm"></i>' : '-'}</td>
|
||||
<td class="text-center">
|
||||
<div class="flex gap-2 justify-center items-center">
|
||||
<button class="btn btn-outline btn-sm btn-edit-user" data-idx="${idx}">수정</button>
|
||||
<button class="btn-circle-remove btn-del-user" data-idx="${idx}">×</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('.btn-edit-user').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt((e.currentTarget as HTMLElement).getAttribute('data-idx')!);
|
||||
this.openUserEditSubModal(idx);
|
||||
});
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('.btn-del-user').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt((e.currentTarget as HTMLElement).getAttribute('data-idx')!);
|
||||
if (confirm('사용자 할당을 삭제하시겠습니까?')) {
|
||||
this.tempSwUsers.splice(idx, 1); this.renderUserList();
|
||||
}
|
||||
});
|
||||
});
|
||||
createIcons({ icons: { Paperclip } });
|
||||
}
|
||||
|
||||
private openUserEditSubModal(idx: number = -1) {
|
||||
const subModal = document.getElementById('sw-user-edit-modal')!;
|
||||
const form = document.getElementById('sw-user-edit-form') as HTMLFormElement;
|
||||
form.reset();
|
||||
setFieldValue('edit-user-index', idx);
|
||||
if (idx > -1) {
|
||||
const user = this.tempSwUsers[idx];
|
||||
setFieldValue('new-user-조직', user.조직);
|
||||
setFieldValue('new-user-부서', user.부서);
|
||||
setFieldValue('new-user-직위', user.직위);
|
||||
setFieldValue('new-user-이름', user.이름);
|
||||
if (user.사용기간 && user.사용기간.includes('~')) {
|
||||
const parts = user.사용기간.split('~');
|
||||
setFieldValue('new-user-시작일', parts[0].trim());
|
||||
setFieldValue('new-user-종료일', parts[1].trim());
|
||||
}
|
||||
}
|
||||
subModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
private saveUserDataToList() {
|
||||
const idx = parseInt(getFieldValue('edit-user-index'));
|
||||
const 신청서Input = document.getElementById('new-user-신청서') as HTMLInputElement;
|
||||
const 신청서명 = 신청서Input.files && 신청서Input.files.length > 0 ? 신청서Input.files[0].name : (idx > -1 ? this.tempSwUsers[idx].신청서명 : '');
|
||||
|
||||
const userData: any = {
|
||||
조직: getFieldValue('new-user-조직'),
|
||||
부서: getFieldValue('new-user-부서'),
|
||||
직위: getFieldValue('new-user-직위'),
|
||||
이름: getFieldValue('new-user-이름'),
|
||||
사용기간: `${getFieldValue('new-user-시작일')} ~ ${getFieldValue('new-user-종료일')}`,
|
||||
신청서명
|
||||
};
|
||||
if (idx === -1) this.tempSwUsers.push(userData);
|
||||
else this.tempSwUsers[idx] = userData;
|
||||
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
|
||||
this.renderUserList();
|
||||
}
|
||||
}
|
||||
|
||||
export const swUserModal = new SwUserModal();
|
||||
export function initSwUserModal(onSave: () => void, closeModals: () => void) { swUserModal.init(onSave, closeModals); }
|
||||
export function openSwUserModal(asset: any) { swUserModal.open(asset); }
|
||||
import { state } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { createIcons, Edit2, X, Paperclip, Calendar, Plus } from 'lucide';
|
||||
import { ORG_LIST } from './SharedData';
|
||||
import { generateOptionsHTML, setFieldValue, getFieldValue, applyDateMask } from './ModalUtils';
|
||||
|
||||
class SwUserModal extends BaseModal {
|
||||
private tempSwUsers: any[] = [];
|
||||
|
||||
constructor() {
|
||||
super('sw-user', '소프트웨어 사용자 관리');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="sw-user-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<h2 id="sw-user-title" class="modal-title">${this.title}</h2>
|
||||
<button id="btn-close-sw-user-modal" class="btn-icon" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="sw-info-summary" id="sw-user-sw-info"></div>
|
||||
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="detail-section-title mb-0">할당된 사용자 목록</h3>
|
||||
<button type="button" id="btn-open-add-user" class="btn btn-primary btn-sm"><i data-lucide="plus" class="icon-sm"></i> 사용자 추가</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>조직</th>
|
||||
<th>부서</th>
|
||||
<th>직위</th>
|
||||
<th>이름</th>
|
||||
<th class="text-center">사용기간</th>
|
||||
<th class="text-center">신청서</th>
|
||||
<th class="text-center">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sw-user-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- 더미 폼 (BaseModal 필수 요건 충족용) -->
|
||||
<form id="sw-user-asset-form" class="hidden"></form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-cancel-sw-user" class="btn btn-outline">취소</button>
|
||||
<button id="btn-save-sw-user" class="btn btn-primary">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사용자 추가/수정 서브 모달 -->
|
||||
<div id="sw-user-edit-modal" class="modal-overlay hidden sub-modal">
|
||||
<div class="modal-content narrow">
|
||||
<div class="modal-header">
|
||||
<h3 id="sw-user-edit-title" class="modal-title">사용자 정보</h3>
|
||||
<button id="btn-close-user-edit" class="btn-icon">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="sw-user-edit-form" class="grid-form vertical-form">
|
||||
<input type="hidden" id="edit-user-index" value="-1" />
|
||||
<div class="form-group">
|
||||
<label>조직</label>
|
||||
<select id="new-user-조직">${generateOptionsHTML(ORG_LIST)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>부서</label>
|
||||
<input type="text" id="new-user-부서" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>직위</label>
|
||||
<input type="text" id="new-user-직위" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>이름</label>
|
||||
<input type="text" id="new-user-이름" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>사용 시작일</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="new-user-시작일" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-시작일-picker'); p.value = document.getElementById('new-user-시작일').value; p.showPicker();">
|
||||
<i data-lucide="calendar" class="icon-sm"></i>
|
||||
</button>
|
||||
<input type="date" id="new-user-시작일-picker" class="hidden-picker" onchange="document.getElementById('new-user-시작일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>사용 종료일</label>
|
||||
<div class="input-with-btn">
|
||||
<input type="text" id="new-user-종료일" />
|
||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-종료일-picker'); p.value = document.getElementById('new-user-종료일').value; p.showPicker();">
|
||||
<i data-lucide="calendar" class="icon-sm"></i>
|
||||
</button>
|
||||
<input type="date" id="new-user-종료일-picker" class="hidden-picker" onchange="document.getElementById('new-user-종료일').value = this.value" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>신청서 (증빙)</label>
|
||||
<input type="file" id="new-user-신청서" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-close-user-sub" class="btn btn-outline">취소</button>
|
||||
<button id="btn-confirm-user-edit" class="btn btn-primary">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.hidden-picker {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const mainSaveBtn = document.getElementById('btn-save-sw-user')!;
|
||||
const addUserBtn = document.getElementById('btn-open-add-user')!;
|
||||
const confirmUserBtn = document.getElementById('btn-confirm-user-edit')!;
|
||||
|
||||
['new-user-시작일', 'new-user-종료일'].forEach(id => {
|
||||
const el = document.getElementById(id) as HTMLInputElement;
|
||||
if (el) applyDateMask(el);
|
||||
});
|
||||
|
||||
addUserBtn.addEventListener('click', () => this.openUserEditSubModal());
|
||||
confirmUserBtn.addEventListener('click', () => this.saveUserDataToList());
|
||||
|
||||
mainSaveBtn.addEventListener('click', () => {
|
||||
if (!this.currentAsset) return;
|
||||
const existingIdx = state.masterData.swUsers.findIndex(u => u.sw_id === this.currentAsset!.id);
|
||||
const newMapping = {
|
||||
sw_id: this.currentAsset!.id,
|
||||
userData: this.tempSwUsers.map(u => [u.조직, u.부서, u.직위, u.이름, u.사용기간, u.신청서명])
|
||||
};
|
||||
if (existingIdx > -1) state.masterData.swUsers[existingIdx] = newMapping as any;
|
||||
else state.masterData.swUsers.push(newMapping as any);
|
||||
|
||||
onSave(); this.close(); closeModals();
|
||||
});
|
||||
|
||||
document.getElementById('btn-close-sw-user-modal')?.addEventListener('click', () => this.close());
|
||||
document.getElementById('btn-cancel-sw-user')?.addEventListener('click', () => this.close());
|
||||
|
||||
const subModal = document.getElementById('sw-user-edit-modal')!;
|
||||
const closeSub = () => subModal.classList.add('hidden');
|
||||
document.getElementById('btn-close-user-edit')?.addEventListener('click', closeSub);
|
||||
document.getElementById('btn-close-user-sub')?.addEventListener('click', closeSub);
|
||||
|
||||
createIcons({ icons: { X, Plus, Calendar, Edit2, Paperclip } });
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
const swInfo = document.getElementById('sw-user-sw-info')!;
|
||||
swInfo.innerHTML = `
|
||||
<div class="sw-info-header border-b border-hairline pb-4 mb-6">
|
||||
<div class="detail-label-sm">${asset.purchase_corp || asset.법인 || ''}</div>
|
||||
<div class="asset-code-title">${asset.product_name || asset.제품명 || ''}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const existingMapping = state.masterData.swUsers.find(u => u.sw_id === asset.id);
|
||||
this.tempSwUsers = existingMapping ? (existingMapping.userData || []).map((u: any) => ({
|
||||
조직: u[0], 부서: u[1], 직위: u[2], 이름: u[3], 사용기간: u[4], 신청서명: u[5]
|
||||
})) : [];
|
||||
|
||||
this.renderUserList();
|
||||
}
|
||||
|
||||
protected onAfterOpen(): void {}
|
||||
|
||||
private renderUserList() {
|
||||
const tbody = document.getElementById('sw-user-table-body')!;
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = '';
|
||||
if (this.tempSwUsers.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="empty-cell text-center p-8">할당된 사용자가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.tempSwUsers.forEach((user, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${user.조직 || ''}</td>
|
||||
<td>${user.부서 || ''}</td>
|
||||
<td>${user.직위 || ''}</td>
|
||||
<td>${user.이름 || ''}</td>
|
||||
<td class="text-center">${user.사용기간 || ''}</td>
|
||||
<td class="text-center">${user.신청서명 ? '<i data-lucide="paperclip" class="text-primary icon-sm"></i>' : '-'}</td>
|
||||
<td class="text-center">
|
||||
<div class="flex gap-2 justify-center items-center">
|
||||
<button class="btn btn-outline btn-sm btn-edit-user" data-idx="${idx}">수정</button>
|
||||
<button class="btn-circle-remove btn-del-user" data-idx="${idx}">×</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('.btn-edit-user').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt((e.currentTarget as HTMLElement).getAttribute('data-idx')!);
|
||||
this.openUserEditSubModal(idx);
|
||||
});
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('.btn-del-user').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt((e.currentTarget as HTMLElement).getAttribute('data-idx')!);
|
||||
if (confirm('사용자 할당을 삭제하시겠습니까?')) {
|
||||
this.tempSwUsers.splice(idx, 1); this.renderUserList();
|
||||
}
|
||||
});
|
||||
});
|
||||
createIcons({ icons: { Paperclip } });
|
||||
}
|
||||
|
||||
private openUserEditSubModal(idx: number = -1) {
|
||||
const subModal = document.getElementById('sw-user-edit-modal')!;
|
||||
const form = document.getElementById('sw-user-edit-form') as HTMLFormElement;
|
||||
form.reset();
|
||||
setFieldValue('edit-user-index', idx);
|
||||
if (idx > -1) {
|
||||
const user = this.tempSwUsers[idx];
|
||||
setFieldValue('new-user-조직', user.조직);
|
||||
setFieldValue('new-user-부서', user.부서);
|
||||
setFieldValue('new-user-직위', user.직위);
|
||||
setFieldValue('new-user-이름', user.이름);
|
||||
if (user.사용기간 && user.사용기간.includes('~')) {
|
||||
const parts = user.사용기간.split('~');
|
||||
setFieldValue('new-user-시작일', parts[0].trim());
|
||||
setFieldValue('new-user-종료일', parts[1].trim());
|
||||
}
|
||||
}
|
||||
subModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
private saveUserDataToList() {
|
||||
const idx = parseInt(getFieldValue('edit-user-index'));
|
||||
const 신청서Input = document.getElementById('new-user-신청서') as HTMLInputElement;
|
||||
const 신청서명 = 신청서Input.files && 신청서Input.files.length > 0 ? 신청서Input.files[0].name : (idx > -1 ? this.tempSwUsers[idx].신청서명 : '');
|
||||
|
||||
const userData: any = {
|
||||
조직: getFieldValue('new-user-조직'),
|
||||
부서: getFieldValue('new-user-부서'),
|
||||
직위: getFieldValue('new-user-직위'),
|
||||
이름: getFieldValue('new-user-이름'),
|
||||
사용기간: `${getFieldValue('new-user-시작일')} ~ ${getFieldValue('new-user-종료일')}`,
|
||||
신청서명
|
||||
};
|
||||
if (idx === -1) this.tempSwUsers.push(userData);
|
||||
else this.tempSwUsers[idx] = userData;
|
||||
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
|
||||
this.renderUserList();
|
||||
}
|
||||
}
|
||||
|
||||
export const swUserModal = new SwUserModal();
|
||||
export function initSwUserModal(onSave: () => void, closeModals: () => void) { swUserModal.init(onSave, closeModals); }
|
||||
export function openSwUserModal(asset: any) { swUserModal.open(asset); }
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
/**
|
||||
* 모든 모달에서 공통으로 사용하는 리스트 데이터 및 설정
|
||||
*/
|
||||
|
||||
// 구매법인 목록
|
||||
export const CORP_LIST = ['한맥', '삼안', 'PTC', '바론'];
|
||||
|
||||
// 사용조직 목록
|
||||
export const ORG_LIST = ['한맥', '삼안', '장헌', '한라', 'PTC', '기술개발센터', '총괄기획실'];
|
||||
|
||||
// 하드웨어 상태 목록
|
||||
export const HW_STATUS_LIST = ['운영', '재고', '수리', '폐기', '기타'];
|
||||
|
||||
// 구분(Category) -> 유형(Asset Type) 관계 정의 (통합 관리)
|
||||
export const CATEGORY_TYPE_MAP: Record<string, string[]> = {
|
||||
'서버': ['서버 렉', '가상서버(VM)', '워크스테이션', '저장시스템_렉(NAS)', '저장시스템_렉(DAS)', '저장시스템_미니(NAS)', '저장시스템_미니(DAS)'],
|
||||
'PC': ['개인PC', '노트북', '공용PC', '서버PC'],
|
||||
'저장매체': ['SSD', 'HDD', '외장HDD'],
|
||||
'네트워크': ['스위치', '허브', '방화벽', '라우터', '공유기', '허브'],
|
||||
'PC부품': ['CPU', 'RAM', 'GPU', 'SSD', 'HDD', 'RAM', '모니터'],
|
||||
'공간정보장비': ['드론', '측량장비', '보조기기'],
|
||||
'업무지원장비': ['카메라', '스피커', 'TV', '모바일', '유선전화기', 'XR', '프린터', '전산소모품'],
|
||||
'외부SW': ['영구', '구독'],
|
||||
'내부SW': ['판매용', 'Solutions', 'Inhouse', 'Engine&Module'],
|
||||
'비용관리': ['클라우드', '도메인', '전화', '인터넷', '이메일'],
|
||||
'내빈/외빈': ['선물'],
|
||||
'시설자산': ['사무가구']
|
||||
};
|
||||
|
||||
// 설치위치 종속성 데이터
|
||||
export const LOCATION_DATA: Record<string, string[]> = {
|
||||
'한맥빌딩': ['MDF실', '1층', '2층', '3층', '4층', '5층', '6층', '7층', '파고라'],
|
||||
'기술개발센터': ['서버실', '센터내부'],
|
||||
'유니온빌딩': ['4층', '5층', '6층'],
|
||||
'뉴코아빌딩': ['4층', '6층', '7층'],
|
||||
'IDC': ['서관202', '서관203', '서관204', '서관205', '동관53', '동관54']
|
||||
};
|
||||
|
||||
// 유형별 자산번호 접두사(Prefix) 매핑
|
||||
export const TYPE_PREFIX_MAP: Record<string, string> = {
|
||||
'서버': 'SVR', '워크스테이션': 'SVR', '개인PC': 'PC', '공용PC': 'PC', '서버PC': 'PC',
|
||||
'저장시스템_렉(NAS)': 'DSS', '저장시스템_렉(DAS)': 'DSS', '저장시스템_미니(NAS)': 'DSS', '저장시스템_미니(DAS)': 'DSS',
|
||||
'저장매체': 'STM', 'HDD': 'HDD', 'SSD': 'SSD',
|
||||
'노트북': 'NBK', '태블릿': 'TAB',
|
||||
'드론': 'DRO', '측량장비': 'SUR', '보조기기': 'SUR', '허브': 'NET',
|
||||
'구독SW': 'SW', '영구SW': 'SW', '내부' : 'SW_INT', '외부':'SW_EXT'
|
||||
};
|
||||
|
||||
// 배치도 이미지 매핑 데이터
|
||||
export const IMAGE_LOCATIONS: Record<string, Record<string, string[]>> = {
|
||||
'IDC': {
|
||||
'서관202': ['img/location_photo/IDC/서관202.png'],
|
||||
'서관203': ['img/location_photo/IDC/서관203.png'],
|
||||
'서관204': ['img/location_photo/IDC/서관204.png'],
|
||||
'서관205': ['img/location_photo/IDC/서관205.png'],
|
||||
'동관53': ['img/location_photo/IDC/동관53.png'],
|
||||
'동관54': ['img/location_photo/IDC/동관54.png'],
|
||||
},
|
||||
'기술개발센터': {
|
||||
'서버실': [
|
||||
'img/location_photo/기술개발센터/서버실/서버실_1.png',
|
||||
'img/location_photo/기술개발센터/서버실/서버실_2.png'
|
||||
],
|
||||
'센터내부': ['img/location_photo/기술개발센터/센터내부/센터내부.png']
|
||||
},
|
||||
'한맥빌딩': {
|
||||
'1층': ['img/location_photo/한맥빌딩/1층.png'],
|
||||
'2층': ['img/location_photo/한맥빌딩/2층.png'],
|
||||
'3층': ['img/location_photo/한맥빌딩/3층.png'],
|
||||
'4층': ['img/location_photo/한맥빌딩/4층.png'],
|
||||
'5층': ['img/location_photo/한맥빌딩/5층.png'],
|
||||
'6층': ['img/location_photo/한맥빌딩/6층.png'],
|
||||
'7층': ['img/location_photo/한맥빌딩/7층.png'],
|
||||
'MDF실': [
|
||||
'img/location_photo/한맥빌딩/MDF실/MDF_1.png',
|
||||
'img/location_photo/한맥빌딩/MDF실/MDF_2.png',
|
||||
'img/location_photo/한맥빌딩/MDF실/MDF_3.png',
|
||||
'img/location_photo/한맥빌딩/MDF실/MDF_4.png'
|
||||
]
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 모든 모달에서 공통으로 사용하는 리스트 데이터 및 설정
|
||||
*/
|
||||
|
||||
// 구매법인 목록
|
||||
export const CORP_LIST = ['한맥', '삼안', 'PTC', '바론'];
|
||||
|
||||
// 사용조직 목록
|
||||
export const ORG_LIST = ['한맥', '삼안', '장헌', '한라', 'PTC', '기술개발센터', '총괄기획실'];
|
||||
|
||||
// 하드웨어 상태 목록
|
||||
export const HW_STATUS_LIST = ['운영', '재고', '수리', '폐기', '기타'];
|
||||
|
||||
// 구분(Category) -> 유형(Asset Type) 관계 정의 (통합 관리)
|
||||
export const CATEGORY_TYPE_MAP: Record<string, string[]> = {
|
||||
'서버': ['서버 렉', '가상서버(VM)', '워크스테이션', '저장시스템_렉(NAS)', '저장시스템_렉(DAS)', '저장시스템_미니(NAS)', '저장시스템_미니(DAS)'],
|
||||
'PC': ['개인PC', '노트북', '공용PC', '서버PC'],
|
||||
'저장매체': ['SSD', 'HDD', '외장HDD'],
|
||||
'네트워크': ['스위치', '허브', '방화벽', '라우터', '공유기', '허브'],
|
||||
'PC부품': ['CPU', 'RAM', 'GPU', 'SSD', 'HDD', 'RAM', '모니터'],
|
||||
'공간정보장비': ['드론', '측량장비', '보조기기'],
|
||||
'업무지원장비': ['카메라', '스피커', 'TV', '모바일', '유선전화기', 'XR', '프린터', '전산소모품'],
|
||||
'외부SW': ['영구', '구독'],
|
||||
'내부SW': ['판매용', 'Solutions', 'Inhouse', 'Engine&Module'],
|
||||
'비용관리': ['클라우드', '도메인', '전화', '인터넷', '이메일'],
|
||||
'내빈/외빈': ['선물'],
|
||||
'시설자산': ['사무가구']
|
||||
};
|
||||
|
||||
// 설치위치 종속성 데이터
|
||||
export const LOCATION_DATA: Record<string, string[]> = {
|
||||
'한맥빌딩': ['MDF실', '1층', '2층', '3층', '4층', '5층', '6층', '7층', '파고라'],
|
||||
'기술개발센터': ['서버실', '센터내부'],
|
||||
'유니온빌딩': ['4층', '5층', '6층'],
|
||||
'뉴코아빌딩': ['4층', '6층', '7층'],
|
||||
'IDC': ['서관202', '서관203', '서관204', '서관205', '동관53', '동관54']
|
||||
};
|
||||
|
||||
// 유형별 자산번호 접두사(Prefix) 매핑
|
||||
export const TYPE_PREFIX_MAP: Record<string, string> = {
|
||||
'서버': 'SVR', '워크스테이션': 'SVR', '개인PC': 'PC', '공용PC': 'PC', '서버PC': 'PC',
|
||||
'저장시스템_렉(NAS)': 'DSS', '저장시스템_렉(DAS)': 'DSS', '저장시스템_미니(NAS)': 'DSS', '저장시스템_미니(DAS)': 'DSS',
|
||||
'저장매체': 'STM', 'HDD': 'HDD', 'SSD': 'SSD',
|
||||
'노트북': 'NBK', '태블릿': 'TAB',
|
||||
'드론': 'DRO', '측량장비': 'SUR', '보조기기': 'SUR', '허브': 'NET',
|
||||
'구독SW': 'SW', '영구SW': 'SW', '내부' : 'SW_INT', '외부':'SW_EXT'
|
||||
};
|
||||
|
||||
// 배치도 이미지 매핑 데이터
|
||||
export const IMAGE_LOCATIONS: Record<string, Record<string, string[]>> = {
|
||||
'IDC': {
|
||||
'서관202': ['img/location_photo/IDC/서관202.png'],
|
||||
'서관203': ['img/location_photo/IDC/서관203.png'],
|
||||
'서관204': ['img/location_photo/IDC/서관204.png'],
|
||||
'서관205': ['img/location_photo/IDC/서관205.png'],
|
||||
'동관53': ['img/location_photo/IDC/동관53.png'],
|
||||
'동관54': ['img/location_photo/IDC/동관54.png'],
|
||||
},
|
||||
'기술개발센터': {
|
||||
'서버실': [
|
||||
'img/location_photo/기술개발센터/서버실/서버실_1.png',
|
||||
'img/location_photo/기술개발센터/서버실/서버실_2.png'
|
||||
],
|
||||
'센터내부': ['img/location_photo/기술개발센터/센터내부/센터내부.png']
|
||||
},
|
||||
'한맥빌딩': {
|
||||
'1층': ['img/location_photo/한맥빌딩/1층.png'],
|
||||
'2층': ['img/location_photo/한맥빌딩/2층.png'],
|
||||
'3층': ['img/location_photo/한맥빌딩/3층.png'],
|
||||
'4층': ['img/location_photo/한맥빌딩/4층.png'],
|
||||
'5층': ['img/location_photo/한맥빌딩/5층.png'],
|
||||
'6층': ['img/location_photo/한맥빌딩/6층.png'],
|
||||
'7층': ['img/location_photo/한맥빌딩/7층.png'],
|
||||
'MDF실': [
|
||||
'img/location_photo/한맥빌딩/MDF실/MDF_1.png',
|
||||
'img/location_photo/한맥빌딩/MDF실/MDF_2.png',
|
||||
'img/location_photo/한맥빌딩/MDF실/MDF_3.png',
|
||||
'img/location_photo/한맥빌딩/MDF실/MDF_4.png'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,180 +1,180 @@
|
||||
import { state, saveSystemUser, deleteSystemUser } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { setFieldValue } from './ModalUtils';
|
||||
import { createIcons, X, Save } from 'lucide';
|
||||
import { UI_TEXT } from '../../core/schema';
|
||||
|
||||
class UserModal extends BaseModal {
|
||||
constructor() {
|
||||
super('user', '임직원 정보');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="user-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content narrow">
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<h2 id="user-modal-title" class="modal-title">${this.title}</h2>
|
||||
<div id="user-header-identity" class="header-identity"></div>
|
||||
</div>
|
||||
<button id="btn-close-user-modal" class="btn-icon" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="user-asset-form" class="grid-form vertical-form">
|
||||
<input type="hidden" id="user-id" name="id" />
|
||||
|
||||
<div class="form-group">
|
||||
<label>사번</label>
|
||||
<input type="text" id="user-emp-no" name="emp_no" placeholder="예: HM202601" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>사용자명</label>
|
||||
<input type="text" id="user-name-input" name="user_name" placeholder="예: 홍길동" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>사용조직 (부서)</label>
|
||||
<input type="text" id="user-dept" name="dept_name" placeholder="예: 기술개발센터" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>직무 (직급)</label>
|
||||
<input type="text" id="user-position-input" name="position" placeholder="예: BIM모델러" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>상태</label>
|
||||
<select id="user-status" name="status">
|
||||
<option value="재직">재직</option>
|
||||
<option value="퇴직">퇴직</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-user-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-revert-user-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-cancel-user-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-user-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const saveBtn = document.getElementById('btn-save-user-asset')!;
|
||||
const revertBtn = document.getElementById('btn-revert-user-edit')!;
|
||||
const deleteBtn = document.getElementById('btn-delete-user-asset')!;
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset) return;
|
||||
if (!this.isEditMode) {
|
||||
this.setEditLockMode('edit');
|
||||
this.isEditMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const empNo = (document.getElementById('user-emp-no') as HTMLInputElement).value.trim();
|
||||
const userName = (document.getElementById('user-name-input') as HTMLInputElement).value.trim();
|
||||
const deptName = (document.getElementById('user-dept') as HTMLInputElement).value.trim();
|
||||
const position = (document.getElementById('user-position-input') as HTMLInputElement).value.trim();
|
||||
const status = (document.getElementById('user-status') as HTMLSelectElement).value;
|
||||
|
||||
if (!empNo || !userName || !deptName || !position) {
|
||||
alert('모든 필수 입력 필드를 채워주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = {
|
||||
id: this.currentAsset.id || null,
|
||||
emp_no: empNo,
|
||||
user_name: userName,
|
||||
dept_name: deptName,
|
||||
position: position,
|
||||
status: status
|
||||
};
|
||||
|
||||
if (await saveSystemUser(updated)) {
|
||||
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
revertBtn.addEventListener('click', () => {
|
||||
this.setEditLockMode('view');
|
||||
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset || !this.currentAsset.id) return;
|
||||
if (!confirm('정말로 이 임직원 정보를 삭제하시겠습니까?')) return;
|
||||
|
||||
if (await deleteSystemUser(this.currentAsset.id)) {
|
||||
alert('성공적으로 삭제되었습니다.');
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
createIcons({ icons: { Save, X } });
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
setFieldValue('user-id', asset.id || '');
|
||||
setFieldValue('user-emp-no', asset.emp_no || '');
|
||||
setFieldValue('user-name-input', asset.user_name || '');
|
||||
setFieldValue('user-dept', asset.dept_name || '');
|
||||
setFieldValue('user-position-input', asset.position || '');
|
||||
setFieldValue('user-status', asset.status || '재직');
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
protected onAfterOpen(asset: any, mode: string): void {
|
||||
const titleEl = document.getElementById('user-modal-title');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = (mode === 'add') ? '신규 임직원 등록' : '임직원 정보 수정';
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('btn-delete-user-asset')!;
|
||||
const saveBtn = document.getElementById('btn-save-user-asset')!;
|
||||
|
||||
deleteBtn.style.display = (mode === 'add') ? 'none' : 'block';
|
||||
|
||||
if (mode === 'add' || mode === 'edit') {
|
||||
saveBtn.textContent = mode === 'add' ? '등록' : '저장';
|
||||
saveBtn.style.display = 'block';
|
||||
} else {
|
||||
saveBtn.textContent = '수정';
|
||||
saveBtn.style.display = 'block';
|
||||
}
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
private updateHeaderIdentity(asset: any) {
|
||||
const container = document.getElementById('user-header-identity');
|
||||
if (!container) return;
|
||||
|
||||
if (this.currentMode === 'add') {
|
||||
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const empNo = asset.emp_no || '';
|
||||
const userName = asset.user_name || '';
|
||||
const dept = asset.dept_name || '';
|
||||
|
||||
container.innerHTML = `
|
||||
<span class="asset-code-title">${userName}</span>
|
||||
<span class="service-type-badge">${empNo}</span>
|
||||
<span class="asset-type-label">${dept}</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const userModal = new UserModal();
|
||||
export function initUserModal(onSave: () => void, closeModals: () => void) { userModal.init(onSave, closeModals); }
|
||||
export function openUserModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') { userModal.open(asset, mode); }
|
||||
import { state, saveSystemUser, deleteSystemUser } from '../../core/state';
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { setFieldValue } from './ModalUtils';
|
||||
import { createIcons, X, Save } from 'lucide';
|
||||
import { UI_TEXT } from '../../core/schema';
|
||||
|
||||
class UserModal extends BaseModal {
|
||||
constructor() {
|
||||
super('user', '임직원 정보');
|
||||
}
|
||||
|
||||
protected renderFrameHTML(): string {
|
||||
return `
|
||||
<div id="user-asset-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content narrow">
|
||||
<div class="modal-header">
|
||||
<div class="header-left">
|
||||
<h2 id="user-modal-title" class="modal-title">${this.title}</h2>
|
||||
<div id="user-header-identity" class="header-identity"></div>
|
||||
</div>
|
||||
<button id="btn-close-user-modal" class="btn-icon" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="user-asset-form" class="grid-form vertical-form">
|
||||
<input type="hidden" id="user-id" name="id" />
|
||||
|
||||
<div class="form-group">
|
||||
<label>사번</label>
|
||||
<input type="text" id="user-emp-no" name="emp_no" placeholder="예: HM202601" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>사용자명</label>
|
||||
<input type="text" id="user-name-input" name="user_name" placeholder="예: 홍길동" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>사용조직 (부서)</label>
|
||||
<input type="text" id="user-dept" name="dept_name" placeholder="예: 기술개발센터" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>직무 (직급)</label>
|
||||
<input type="text" id="user-position-input" name="position" placeholder="예: BIM모델러" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>상태</label>
|
||||
<select id="user-status" name="status">
|
||||
<option value="재직">재직</option>
|
||||
<option value="퇴직">퇴직</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btn-delete-user-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||
<div class="footer-actions">
|
||||
<button id="btn-revert-user-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||
<button id="btn-cancel-user-modal" class="btn btn-outline">닫기</button>
|
||||
<button id="btn-save-user-asset" class="btn btn-primary">수정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||
const saveBtn = document.getElementById('btn-save-user-asset')!;
|
||||
const revertBtn = document.getElementById('btn-revert-user-edit')!;
|
||||
const deleteBtn = document.getElementById('btn-delete-user-asset')!;
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset) return;
|
||||
if (!this.isEditMode) {
|
||||
this.setEditLockMode('edit');
|
||||
this.isEditMode = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const empNo = (document.getElementById('user-emp-no') as HTMLInputElement).value.trim();
|
||||
const userName = (document.getElementById('user-name-input') as HTMLInputElement).value.trim();
|
||||
const deptName = (document.getElementById('user-dept') as HTMLInputElement).value.trim();
|
||||
const position = (document.getElementById('user-position-input') as HTMLInputElement).value.trim();
|
||||
const status = (document.getElementById('user-status') as HTMLSelectElement).value;
|
||||
|
||||
if (!empNo || !userName || !deptName || !position) {
|
||||
alert('모든 필수 입력 필드를 채워주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = {
|
||||
id: this.currentAsset.id || null,
|
||||
emp_no: empNo,
|
||||
user_name: userName,
|
||||
dept_name: deptName,
|
||||
position: position,
|
||||
status: status
|
||||
};
|
||||
|
||||
if (await saveSystemUser(updated)) {
|
||||
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
revertBtn.addEventListener('click', () => {
|
||||
this.setEditLockMode('view');
|
||||
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (!this.currentAsset || !this.currentAsset.id) return;
|
||||
if (!confirm('정말로 이 임직원 정보를 삭제하시겠습니까?')) return;
|
||||
|
||||
if (await deleteSystemUser(this.currentAsset.id)) {
|
||||
alert('성공적으로 삭제되었습니다.');
|
||||
onSave(); this.close(); closeModals();
|
||||
}
|
||||
});
|
||||
|
||||
createIcons({ icons: { Save, X } });
|
||||
}
|
||||
|
||||
protected fillFormData(asset: any): void {
|
||||
setFieldValue('user-id', asset.id || '');
|
||||
setFieldValue('user-emp-no', asset.emp_no || '');
|
||||
setFieldValue('user-name-input', asset.user_name || '');
|
||||
setFieldValue('user-dept', asset.dept_name || '');
|
||||
setFieldValue('user-position-input', asset.position || '');
|
||||
setFieldValue('user-status', asset.status || '재직');
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
protected onAfterOpen(asset: any, mode: string): void {
|
||||
const titleEl = document.getElementById('user-modal-title');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = (mode === 'add') ? '신규 임직원 등록' : '임직원 정보 수정';
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('btn-delete-user-asset')!;
|
||||
const saveBtn = document.getElementById('btn-save-user-asset')!;
|
||||
|
||||
deleteBtn.style.display = (mode === 'add') ? 'none' : 'block';
|
||||
|
||||
if (mode === 'add' || mode === 'edit') {
|
||||
saveBtn.textContent = mode === 'add' ? '등록' : '저장';
|
||||
saveBtn.style.display = 'block';
|
||||
} else {
|
||||
saveBtn.textContent = '수정';
|
||||
saveBtn.style.display = 'block';
|
||||
}
|
||||
this.updateHeaderIdentity(asset);
|
||||
}
|
||||
|
||||
private updateHeaderIdentity(asset: any) {
|
||||
const container = document.getElementById('user-header-identity');
|
||||
if (!container) return;
|
||||
|
||||
if (this.currentMode === 'add') {
|
||||
container.innerHTML = '<span class="badge badge-primary">신규 등록</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const empNo = asset.emp_no || '';
|
||||
const userName = asset.user_name || '';
|
||||
const dept = asset.dept_name || '';
|
||||
|
||||
container.innerHTML = `
|
||||
<span class="asset-code-title">${userName}</span>
|
||||
<span class="service-type-badge">${empNo}</span>
|
||||
<span class="asset-type-label">${dept}</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const userModal = new UserModal();
|
||||
export function initUserModal(onSave: () => void, closeModals: () => void) { userModal.init(onSave, closeModals); }
|
||||
export function openUserModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') { userModal.open(asset, mode); }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,120 +1,120 @@
|
||||
import { state } from '../core/state';
|
||||
|
||||
const MENU_CONFIG: any = {
|
||||
hw: {
|
||||
label: '하드웨어',
|
||||
tabs: ['대시보드', '서버', 'PC', '스토리지', '공간정보장비', 'PC부품', '부품 마스터', '네트워크', '업무지원장비']
|
||||
},
|
||||
sw: {
|
||||
label: '소프트웨어',
|
||||
tabs: ['외부SW', '내부SW']
|
||||
},
|
||||
ops: {
|
||||
label: '운영지원',
|
||||
tabs: ['클라우드', '도메인', '비용관리', '사용자']
|
||||
},
|
||||
vip: {
|
||||
label: '내빈/외빈',
|
||||
tabs: ['선물']
|
||||
},
|
||||
fac: {
|
||||
label: '시설자산',
|
||||
tabs: ['사무가구']
|
||||
}
|
||||
};
|
||||
|
||||
export function renderNavigation(onTabChange: (tab: string) => void) {
|
||||
const header = document.querySelector('.main-header') as HTMLElement;
|
||||
const headerContainer = document.querySelector('.header-container')!;
|
||||
if (!headerContainer) return;
|
||||
|
||||
const render = () => {
|
||||
// 1. 헤더 구조 (Vercel Style: Clean Single Row)
|
||||
headerContainer.innerHTML = `
|
||||
<div class="brand" id="btn-home-logo" style="cursor: pointer;">
|
||||
<img src="img/image_92.png" class="main-logo" alt="HM Logo" />
|
||||
<h1>한맥자산관리시스템</h1>
|
||||
</div>
|
||||
|
||||
<nav class="integrated-nav" id="main-nav-list"></nav>
|
||||
|
||||
<div class="header-actions">
|
||||
<div class="role-toggle-wrapper">
|
||||
<span class="role-label user ${state.currentUserRole === 'user' ? 'active' : ''}">실무자</span>
|
||||
<label class="role-toggle">
|
||||
<input type="checkbox" id="role-toggle-checkbox" ${state.currentUserRole === 'admin' ? 'checked' : ''}>
|
||||
<span class="role-slider"></span>
|
||||
</label>
|
||||
<span class="role-label admin ${state.currentUserRole === 'admin' ? 'active' : ''}">관리자</span>
|
||||
</div>
|
||||
<div class="notification-area">
|
||||
<button class="icon-btn" title="알림"><i data-lucide="bell" style="width:18px; height:18px;"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const navList = document.getElementById('main-nav-list')!;
|
||||
|
||||
// 2. GNB 메뉴 렌더링 (Ghost Tab Style)
|
||||
Object.keys(MENU_CONFIG).forEach(catKey => {
|
||||
const config = MENU_CONFIG[catKey];
|
||||
|
||||
const visibleTabs = config.tabs.filter((tab: string) => {
|
||||
if (state.currentUserRole === 'admin') return tab === '대시보드';
|
||||
return tab !== '대시보드';
|
||||
});
|
||||
|
||||
if (visibleTabs.length === 0) return;
|
||||
|
||||
visibleTabs.forEach((tab: string) => {
|
||||
if (tab === '부품 마스터') return;
|
||||
const item = document.createElement('div');
|
||||
const isActive = state.activeSubTab === tab;
|
||||
item.className = `gnb-trigger ${isActive ? 'active' : ''}`;
|
||||
item.textContent = tab;
|
||||
item.style.fontSize = 'var(--fs-sm)'; // Ensure small but standard font
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
state.activeCategory = catKey as any;
|
||||
state.activeSubTab = tab;
|
||||
render();
|
||||
onTabChange(tab);
|
||||
});
|
||||
navList.appendChild(item);
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 관리자 전용 '관리도구'
|
||||
if (state.currentUserRole === 'admin') {
|
||||
const adminTrigger = document.createElement('div');
|
||||
adminTrigger.className = 'gnb-trigger admin-trigger';
|
||||
adminTrigger.innerHTML = '관리도구';
|
||||
adminTrigger.addEventListener('click', () => window.open('/map_editor.html', '_blank'));
|
||||
navList.appendChild(adminTrigger);
|
||||
}
|
||||
|
||||
// 4. 이벤트 바인딩
|
||||
document.getElementById('btn-home-logo')?.addEventListener('click', () => location.reload());
|
||||
|
||||
const roleToggle = document.getElementById('role-toggle-checkbox') as HTMLInputElement;
|
||||
roleToggle?.addEventListener('change', () => {
|
||||
state.currentUserRole = roleToggle.checked ? 'admin' : 'user';
|
||||
if (state.currentUserRole === 'admin') {
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '대시보드';
|
||||
} else {
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '서버';
|
||||
}
|
||||
render();
|
||||
onTabChange(state.activeSubTab);
|
||||
});
|
||||
|
||||
// 아이콘 생성
|
||||
// @ts-ignore
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
};
|
||||
|
||||
render();
|
||||
}
|
||||
import { state } from '../core/state';
|
||||
|
||||
const MENU_CONFIG: any = {
|
||||
hw: {
|
||||
label: '하드웨어',
|
||||
tabs: ['대시보드', '서버', 'PC', '스토리지', '공간정보장비', 'PC부품', '부품 마스터', '네트워크', '업무지원장비']
|
||||
},
|
||||
sw: {
|
||||
label: '소프트웨어',
|
||||
tabs: ['외부SW', '내부SW']
|
||||
},
|
||||
ops: {
|
||||
label: '운영지원',
|
||||
tabs: ['클라우드', '도메인', '비용관리', '사용자']
|
||||
},
|
||||
vip: {
|
||||
label: '내빈/외빈',
|
||||
tabs: ['선물']
|
||||
},
|
||||
fac: {
|
||||
label: '시설자산',
|
||||
tabs: ['사무가구']
|
||||
}
|
||||
};
|
||||
|
||||
export function renderNavigation(onTabChange: (tab: string) => void) {
|
||||
const header = document.querySelector('.main-header') as HTMLElement;
|
||||
const headerContainer = document.querySelector('.header-container')!;
|
||||
if (!headerContainer) return;
|
||||
|
||||
const render = () => {
|
||||
// 1. 헤더 구조 (Vercel Style: Clean Single Row)
|
||||
headerContainer.innerHTML = `
|
||||
<div class="brand" id="btn-home-logo" style="cursor: pointer;">
|
||||
<img src="img/image_92.png" class="main-logo" alt="HM Logo" />
|
||||
<h1>한맥자산관리시스템</h1>
|
||||
</div>
|
||||
|
||||
<nav class="integrated-nav" id="main-nav-list"></nav>
|
||||
|
||||
<div class="header-actions">
|
||||
<div class="role-toggle-wrapper">
|
||||
<span class="role-label user ${state.currentUserRole === 'user' ? 'active' : ''}">실무자</span>
|
||||
<label class="role-toggle">
|
||||
<input type="checkbox" id="role-toggle-checkbox" ${state.currentUserRole === 'admin' ? 'checked' : ''}>
|
||||
<span class="role-slider"></span>
|
||||
</label>
|
||||
<span class="role-label admin ${state.currentUserRole === 'admin' ? 'active' : ''}">관리자</span>
|
||||
</div>
|
||||
<div class="notification-area">
|
||||
<button class="icon-btn" title="알림"><i data-lucide="bell" style="width:18px; height:18px;"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const navList = document.getElementById('main-nav-list')!;
|
||||
|
||||
// 2. GNB 메뉴 렌더링 (Ghost Tab Style)
|
||||
Object.keys(MENU_CONFIG).forEach(catKey => {
|
||||
const config = MENU_CONFIG[catKey];
|
||||
|
||||
const visibleTabs = config.tabs.filter((tab: string) => {
|
||||
if (state.currentUserRole === 'admin') return tab === '대시보드';
|
||||
return tab !== '대시보드';
|
||||
});
|
||||
|
||||
if (visibleTabs.length === 0) return;
|
||||
|
||||
visibleTabs.forEach((tab: string) => {
|
||||
if (tab === '부품 마스터') return;
|
||||
const item = document.createElement('div');
|
||||
const isActive = state.activeSubTab === tab;
|
||||
item.className = `gnb-trigger ${isActive ? 'active' : ''}`;
|
||||
item.textContent = tab;
|
||||
item.style.fontSize = 'var(--fs-sm)'; // Ensure small but standard font
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
state.activeCategory = catKey as any;
|
||||
state.activeSubTab = tab;
|
||||
render();
|
||||
onTabChange(tab);
|
||||
});
|
||||
navList.appendChild(item);
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 관리자 전용 '관리도구'
|
||||
if (state.currentUserRole === 'admin') {
|
||||
const adminTrigger = document.createElement('div');
|
||||
adminTrigger.className = 'gnb-trigger admin-trigger';
|
||||
adminTrigger.innerHTML = '관리도구';
|
||||
adminTrigger.addEventListener('click', () => window.open('/map_editor.html', '_blank'));
|
||||
navList.appendChild(adminTrigger);
|
||||
}
|
||||
|
||||
// 4. 이벤트 바인딩
|
||||
document.getElementById('btn-home-logo')?.addEventListener('click', () => location.reload());
|
||||
|
||||
const roleToggle = document.getElementById('role-toggle-checkbox') as HTMLInputElement;
|
||||
roleToggle?.addEventListener('change', () => {
|
||||
state.currentUserRole = roleToggle.checked ? 'admin' : 'user';
|
||||
if (state.currentUserRole === 'admin') {
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '대시보드';
|
||||
} else {
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '서버';
|
||||
}
|
||||
render();
|
||||
onTabChange(state.activeSubTab);
|
||||
});
|
||||
|
||||
// 아이콘 생성
|
||||
// @ts-ignore
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
};
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -1,188 +1,188 @@
|
||||
/* ITAM Guide Modal Styles - Updated to match common modal style */
|
||||
|
||||
/* Tab Container (below header) */
|
||||
.guide-tabs-container {
|
||||
background: #FAFAFA;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 0 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.guide-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.guide-tabs::-webkit-scrollbar { display: none; }
|
||||
|
||||
.guide-tab {
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.guide-tab:hover {
|
||||
color: var(--primary-color);
|
||||
background: rgba(30, 81, 73, 0.04);
|
||||
}
|
||||
|
||||
.guide-tab.active {
|
||||
color: var(--primary-color);
|
||||
border-bottom-color: var(--primary-color);
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Content Area */
|
||||
.guide-body {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.guide-tab-panel {
|
||||
display: none;
|
||||
padding: 1.5rem 0;
|
||||
animation: guideFadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.guide-tab-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes guideFadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
.guide-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.guide-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.guide-section h3 {
|
||||
font-size: 1.73rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.guide-text {
|
||||
font-size: 24px;
|
||||
color: var(--text-main);
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Flowchart Styles */
|
||||
.flow-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.flow-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
flex: 1;
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.flow-step .step-number {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
font-size: 23px;
|
||||
font-weight: 800;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flow-step .step-label {
|
||||
font-weight: 800;
|
||||
color: var(--text-main);
|
||||
font-size: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.flow-step .step-desc {
|
||||
font-size: 23px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.flow-arrow-right {
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Info Table Style */
|
||||
.guide-info-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.guide-info-table th {
|
||||
background: #f8faf9;
|
||||
color: var(--primary-color);
|
||||
font-weight: 800;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.guide-info-table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
/* Tip Box Style */
|
||||
.guide-tip {
|
||||
background: var(--primary-light);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
padding: 1rem;
|
||||
font-size: 24px;
|
||||
color: var(--primary-color);
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* ITAM Guide Modal Styles - Updated to match common modal style */
|
||||
|
||||
/* Tab Container (below header) */
|
||||
.guide-tabs-container {
|
||||
background: #FAFAFA;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 0 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.guide-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.guide-tabs::-webkit-scrollbar { display: none; }
|
||||
|
||||
.guide-tab {
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.guide-tab:hover {
|
||||
color: var(--primary-color);
|
||||
background: rgba(30, 81, 73, 0.04);
|
||||
}
|
||||
|
||||
.guide-tab.active {
|
||||
color: var(--primary-color);
|
||||
border-bottom-color: var(--primary-color);
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Content Area */
|
||||
.guide-body {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.guide-tab-panel {
|
||||
display: none;
|
||||
padding: 1.5rem 0;
|
||||
animation: guideFadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.guide-tab-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes guideFadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
.guide-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.guide-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.guide-section h3 {
|
||||
font-size: 1.73rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.guide-text {
|
||||
font-size: 24px;
|
||||
color: var(--text-main);
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Flowchart Styles */
|
||||
.flow-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.flow-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
flex: 1;
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.flow-step .step-number {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
font-size: 23px;
|
||||
font-weight: 800;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flow-step .step-label {
|
||||
font-weight: 800;
|
||||
color: var(--text-main);
|
||||
font-size: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.flow-step .step-desc {
|
||||
font-size: 23px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.flow-arrow-right {
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Info Table Style */
|
||||
.guide-info-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.guide-info-table th {
|
||||
background: #f8faf9;
|
||||
color: var(--primary-color);
|
||||
font-weight: 800;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.guide-info-table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
/* Tip Box Style */
|
||||
.guide-tip {
|
||||
background: var(--primary-light);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
padding: 1rem;
|
||||
font-size: 24px;
|
||||
color: var(--primary-color);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/**
|
||||
* ITAM 엑셀 핸들러 (지정 날짜 포맷팅 유틸리티)
|
||||
*/
|
||||
|
||||
export function formatExcelDate(val: any): string {
|
||||
if (!val) return '';
|
||||
if (typeof val === 'number') {
|
||||
const date = new Date(Math.round((val - 25569) * 86400 * 1000));
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
return val.replace(/\./g, '-').trim();
|
||||
}
|
||||
return String(val);
|
||||
}
|
||||
/**
|
||||
* ITAM 엑셀 핸들러 (지정 날짜 포맷팅 유틸리티)
|
||||
*/
|
||||
|
||||
export function formatExcelDate(val: any): string {
|
||||
if (!val) return '';
|
||||
if (typeof val === 'number') {
|
||||
const date = new Date(Math.round((val - 25569) * 86400 * 1000));
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
return val.replace(/\./g, '-').trim();
|
||||
}
|
||||
return String(val);
|
||||
}
|
||||
|
||||
@@ -1,162 +1,162 @@
|
||||
import { ASSET_SCHEMA, UI_TEXT } from './schema';
|
||||
import { generateOptionsHTML } from '../components/Modal/ModalUtils';
|
||||
import { CORP_LIST } from '../components/Modal/SharedData';
|
||||
|
||||
/**
|
||||
* ITAM Unified Filter Bar Component
|
||||
* 검색 UI를 표준화하고 한 곳에서 관리합니다.
|
||||
*/
|
||||
|
||||
export interface FilterOptions {
|
||||
keywordLabel?: string;
|
||||
showCorp?: boolean;
|
||||
showDept?: boolean;
|
||||
showLoc?: boolean;
|
||||
showField?: boolean;
|
||||
showType?: boolean;
|
||||
showStatus?: boolean;
|
||||
extraHTML?: string;
|
||||
onFilterChange: (filters: any) => void;
|
||||
initialFilters?: any;
|
||||
fullList?: any[]; // For populating dynamic filters
|
||||
}
|
||||
|
||||
/**
|
||||
* 전역 액션 버튼 그룹 생성 (자산 추가 등)
|
||||
*/
|
||||
export function getActionButtonsHTML(): string {
|
||||
return `<div id="filter-bar-actions" class="header-action-group"></div>`;
|
||||
}
|
||||
|
||||
export function renderFilterBar(container: HTMLElement, options: FilterOptions) {
|
||||
const {
|
||||
keywordLabel = '통합 검색',
|
||||
showCorp = false,
|
||||
showDept = false,
|
||||
showLoc = false,
|
||||
showField = false,
|
||||
showType = false,
|
||||
showStatus = false,
|
||||
extraHTML = '',
|
||||
onFilterChange,
|
||||
initialFilters = { keyword: '', corp: '', dept: '', loc: '', field: '', type: '', status: '' },
|
||||
fullList = []
|
||||
} = options;
|
||||
|
||||
container.classList.add('search-bar'); // Restored class
|
||||
|
||||
// Helper to get unique sorted values
|
||||
const getUnique = (key: keyof typeof ASSET_SCHEMA | string) => {
|
||||
const fieldKey = (ASSET_SCHEMA as any)[key]?.key || key;
|
||||
return Array.from(new Set(fullList.map(item => item[fieldKey] || item[(ASSET_SCHEMA as any)[key]?.db]).filter(Boolean))).sort();
|
||||
};
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="search-item flex-1">
|
||||
<label>${keywordLabel}</label>
|
||||
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off" value="${initialFilters.keyword || ''}">
|
||||
</div>
|
||||
${showType ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.ASSET_TYPE.ui}</label>
|
||||
<select id="filter-type">
|
||||
<option value="">전체 유형</option>
|
||||
${getUnique('ASSET_TYPE').map(v => `<option value="${v}" ${initialFilters.type === v ? 'selected' : ''}>${v}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
${showStatus ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.HW_STATUS.ui}</label>
|
||||
<select id="filter-status">
|
||||
<option value="">전체 상태</option>
|
||||
${getUnique('HW_STATUS').map(v => `<option value="${v}" ${initialFilters.status === v ? 'selected' : ''}>${v}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
${showField ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.SW_FIELD.ui}</label>
|
||||
<select id="filter-field">
|
||||
<option value="">전체 분야</option>
|
||||
<option value="업무공통" ${initialFilters.field === '업무공통' ? 'selected' : ''}>업무공통</option>
|
||||
<option value="개발S/W" ${initialFilters.field === '개발S/W' ? 'selected' : ''}>개발S/W</option>
|
||||
<option value="디자인" ${initialFilters.field === '디자인' ? 'selected' : ''}>디자인</option>
|
||||
<option value="설계S/W" ${initialFilters.field === '설계S/W' ? 'selected' : ''}>설계S/W</option>
|
||||
</select>
|
||||
</div>` : ''}
|
||||
${showCorp ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
|
||||
<select id="filter-corp">${generateOptionsHTML(CORP_LIST, initialFilters.corp || '', true)}</select>
|
||||
</div>` : ''}
|
||||
${showLoc ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.LOCATION.ui}</label>
|
||||
<select id="filter-loc">
|
||||
<option value="">전체 위치</option>
|
||||
${getUnique('LOCATION').map(v => `<option value="${v}" ${initialFilters.loc === v ? 'selected' : ''}>${v}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
${showDept ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
|
||||
<select id="filter-dept">
|
||||
<option value="">전체 조직</option>
|
||||
${getUnique('CURRENT_DEPT').map(v => `<option value="${v}" ${initialFilters.dept === v ? 'selected' : ''}>${v}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
${extraHTML}
|
||||
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
|
||||
<i data-lucide="refresh-ccw" class="icon-sm"></i> ${UI_TEXT.ACTION.RESET_FILTER}
|
||||
</button>
|
||||
${getActionButtonsHTML()}
|
||||
`;
|
||||
|
||||
// Bind Events
|
||||
const triggerChange = () => {
|
||||
const filters = {
|
||||
keyword: (container.querySelector('#filter-keyword') as HTMLInputElement)?.value.toLowerCase().trim() || '',
|
||||
corp: (container.querySelector('#filter-corp') as HTMLSelectElement)?.value || '',
|
||||
dept: (container.querySelector('#filter-dept') as HTMLSelectElement)?.value || '',
|
||||
loc: (container.querySelector('#filter-loc') as HTMLSelectElement)?.value || '',
|
||||
field: (container.querySelector('#filter-field') as HTMLSelectElement)?.value || '',
|
||||
type: (container.querySelector('#filter-type') as HTMLSelectElement)?.value || '',
|
||||
status: (container.querySelector('#filter-status') as HTMLSelectElement)?.value || ''
|
||||
};
|
||||
onFilterChange(filters);
|
||||
};
|
||||
|
||||
container.querySelector('#filter-keyword')?.addEventListener('input', triggerChange);
|
||||
container.querySelector('#filter-corp')?.addEventListener('change', triggerChange);
|
||||
container.querySelector('#filter-dept')?.addEventListener('change', triggerChange);
|
||||
container.querySelector('#filter-loc')?.addEventListener('change', triggerChange);
|
||||
container.querySelector('#filter-field')?.addEventListener('change', triggerChange);
|
||||
container.querySelector('#filter-type')?.addEventListener('change', triggerChange);
|
||||
container.querySelector('#filter-status')?.addEventListener('change', triggerChange);
|
||||
|
||||
container.querySelector('#btn-reset-filters')?.addEventListener('click', () => {
|
||||
['filter-keyword', 'filter-corp', 'filter-dept', 'filter-loc', 'filter-field', 'filter-type', 'filter-status'].forEach(id => {
|
||||
const el = container.querySelector(`#${id}`);
|
||||
if (el) (el as any).value = '';
|
||||
});
|
||||
triggerChange();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 필터링 로직
|
||||
*/
|
||||
export function applyCommonFilters(list: any[], filters: any, searchKeys: (keyof typeof ASSET_SCHEMA)[]) {
|
||||
return list.filter(item => {
|
||||
const matchKeyword = !filters.keyword || searchKeys.some(key =>
|
||||
String(item[ASSET_SCHEMA[key].key] || item[ASSET_SCHEMA[key].db] || '').toLowerCase().includes(filters.keyword)
|
||||
);
|
||||
const matchCorp = !filters.corp || (item[ASSET_SCHEMA.PURCHASE_CORP.key] || item[ASSET_SCHEMA.PURCHASE_CORP.db]) === filters.corp;
|
||||
const matchDept = !filters.dept || (item[ASSET_SCHEMA.CURRENT_DEPT.key] || item[ASSET_SCHEMA.CURRENT_DEPT.db]) === filters.dept;
|
||||
const matchLoc = !filters.loc || (item[ASSET_SCHEMA.LOCATION.key] || item[ASSET_SCHEMA.LOCATION.db]) === filters.loc;
|
||||
const matchField = !filters.field || (item[ASSET_SCHEMA.SW_FIELD.key] || item[ASSET_SCHEMA.SW_FIELD.db]) === filters.field;
|
||||
const matchType = !filters.type || (item[ASSET_SCHEMA.ASSET_TYPE.key] || item[ASSET_SCHEMA.ASSET_TYPE.db]) === filters.type;
|
||||
const matchStatus = !filters.status || (item[ASSET_SCHEMA.HW_STATUS.key] || item[ASSET_SCHEMA.HW_STATUS.db]) === filters.status;
|
||||
|
||||
return matchKeyword && matchCorp && matchDept && matchLoc && matchField && matchType && matchStatus;
|
||||
});
|
||||
}
|
||||
import { ASSET_SCHEMA, UI_TEXT } from './schema';
|
||||
import { generateOptionsHTML } from '../components/Modal/ModalUtils';
|
||||
import { CORP_LIST } from '../components/Modal/SharedData';
|
||||
|
||||
/**
|
||||
* ITAM Unified Filter Bar Component
|
||||
* 검색 UI를 표준화하고 한 곳에서 관리합니다.
|
||||
*/
|
||||
|
||||
export interface FilterOptions {
|
||||
keywordLabel?: string;
|
||||
showCorp?: boolean;
|
||||
showDept?: boolean;
|
||||
showLoc?: boolean;
|
||||
showField?: boolean;
|
||||
showType?: boolean;
|
||||
showStatus?: boolean;
|
||||
extraHTML?: string;
|
||||
onFilterChange: (filters: any) => void;
|
||||
initialFilters?: any;
|
||||
fullList?: any[]; // For populating dynamic filters
|
||||
}
|
||||
|
||||
/**
|
||||
* 전역 액션 버튼 그룹 생성 (자산 추가 등)
|
||||
*/
|
||||
export function getActionButtonsHTML(): string {
|
||||
return `<div id="filter-bar-actions" class="header-action-group"></div>`;
|
||||
}
|
||||
|
||||
export function renderFilterBar(container: HTMLElement, options: FilterOptions) {
|
||||
const {
|
||||
keywordLabel = '통합 검색',
|
||||
showCorp = false,
|
||||
showDept = false,
|
||||
showLoc = false,
|
||||
showField = false,
|
||||
showType = false,
|
||||
showStatus = false,
|
||||
extraHTML = '',
|
||||
onFilterChange,
|
||||
initialFilters = { keyword: '', corp: '', dept: '', loc: '', field: '', type: '', status: '' },
|
||||
fullList = []
|
||||
} = options;
|
||||
|
||||
container.classList.add('search-bar'); // Restored class
|
||||
|
||||
// Helper to get unique sorted values
|
||||
const getUnique = (key: keyof typeof ASSET_SCHEMA | string) => {
|
||||
const fieldKey = (ASSET_SCHEMA as any)[key]?.key || key;
|
||||
return Array.from(new Set(fullList.map(item => item[fieldKey] || item[(ASSET_SCHEMA as any)[key]?.db]).filter(Boolean))).sort();
|
||||
};
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="search-item flex-1">
|
||||
<label>${keywordLabel}</label>
|
||||
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off" value="${initialFilters.keyword || ''}">
|
||||
</div>
|
||||
${showType ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.ASSET_TYPE.ui}</label>
|
||||
<select id="filter-type">
|
||||
<option value="">전체 유형</option>
|
||||
${getUnique('ASSET_TYPE').map(v => `<option value="${v}" ${initialFilters.type === v ? 'selected' : ''}>${v}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
${showStatus ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.HW_STATUS.ui}</label>
|
||||
<select id="filter-status">
|
||||
<option value="">전체 상태</option>
|
||||
${getUnique('HW_STATUS').map(v => `<option value="${v}" ${initialFilters.status === v ? 'selected' : ''}>${v}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
${showField ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.SW_FIELD.ui}</label>
|
||||
<select id="filter-field">
|
||||
<option value="">전체 분야</option>
|
||||
<option value="업무공통" ${initialFilters.field === '업무공통' ? 'selected' : ''}>업무공통</option>
|
||||
<option value="개발S/W" ${initialFilters.field === '개발S/W' ? 'selected' : ''}>개발S/W</option>
|
||||
<option value="디자인" ${initialFilters.field === '디자인' ? 'selected' : ''}>디자인</option>
|
||||
<option value="설계S/W" ${initialFilters.field === '설계S/W' ? 'selected' : ''}>설계S/W</option>
|
||||
</select>
|
||||
</div>` : ''}
|
||||
${showCorp ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
|
||||
<select id="filter-corp">${generateOptionsHTML(CORP_LIST, initialFilters.corp || '', true)}</select>
|
||||
</div>` : ''}
|
||||
${showLoc ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.LOCATION.ui}</label>
|
||||
<select id="filter-loc">
|
||||
<option value="">전체 위치</option>
|
||||
${getUnique('LOCATION').map(v => `<option value="${v}" ${initialFilters.loc === v ? 'selected' : ''}>${v}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
${showDept ? `
|
||||
<div class="search-item">
|
||||
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
|
||||
<select id="filter-dept">
|
||||
<option value="">전체 조직</option>
|
||||
${getUnique('CURRENT_DEPT').map(v => `<option value="${v}" ${initialFilters.dept === v ? 'selected' : ''}>${v}</option>`).join('')}
|
||||
</select>
|
||||
</div>` : ''}
|
||||
${extraHTML}
|
||||
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
|
||||
<i data-lucide="refresh-ccw" class="icon-sm"></i> ${UI_TEXT.ACTION.RESET_FILTER}
|
||||
</button>
|
||||
${getActionButtonsHTML()}
|
||||
`;
|
||||
|
||||
// Bind Events
|
||||
const triggerChange = () => {
|
||||
const filters = {
|
||||
keyword: (container.querySelector('#filter-keyword') as HTMLInputElement)?.value.toLowerCase().trim() || '',
|
||||
corp: (container.querySelector('#filter-corp') as HTMLSelectElement)?.value || '',
|
||||
dept: (container.querySelector('#filter-dept') as HTMLSelectElement)?.value || '',
|
||||
loc: (container.querySelector('#filter-loc') as HTMLSelectElement)?.value || '',
|
||||
field: (container.querySelector('#filter-field') as HTMLSelectElement)?.value || '',
|
||||
type: (container.querySelector('#filter-type') as HTMLSelectElement)?.value || '',
|
||||
status: (container.querySelector('#filter-status') as HTMLSelectElement)?.value || ''
|
||||
};
|
||||
onFilterChange(filters);
|
||||
};
|
||||
|
||||
container.querySelector('#filter-keyword')?.addEventListener('input', triggerChange);
|
||||
container.querySelector('#filter-corp')?.addEventListener('change', triggerChange);
|
||||
container.querySelector('#filter-dept')?.addEventListener('change', triggerChange);
|
||||
container.querySelector('#filter-loc')?.addEventListener('change', triggerChange);
|
||||
container.querySelector('#filter-field')?.addEventListener('change', triggerChange);
|
||||
container.querySelector('#filter-type')?.addEventListener('change', triggerChange);
|
||||
container.querySelector('#filter-status')?.addEventListener('change', triggerChange);
|
||||
|
||||
container.querySelector('#btn-reset-filters')?.addEventListener('click', () => {
|
||||
['filter-keyword', 'filter-corp', 'filter-dept', 'filter-loc', 'filter-field', 'filter-type', 'filter-status'].forEach(id => {
|
||||
const el = container.querySelector(`#${id}`);
|
||||
if (el) (el as any).value = '';
|
||||
});
|
||||
triggerChange();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 필터링 로직
|
||||
*/
|
||||
export function applyCommonFilters(list: any[], filters: any, searchKeys: (keyof typeof ASSET_SCHEMA)[]) {
|
||||
return list.filter(item => {
|
||||
const matchKeyword = !filters.keyword || searchKeys.some(key =>
|
||||
String(item[ASSET_SCHEMA[key].key] || item[ASSET_SCHEMA[key].db] || '').toLowerCase().includes(filters.keyword)
|
||||
);
|
||||
const matchCorp = !filters.corp || (item[ASSET_SCHEMA.PURCHASE_CORP.key] || item[ASSET_SCHEMA.PURCHASE_CORP.db]) === filters.corp;
|
||||
const matchDept = !filters.dept || (item[ASSET_SCHEMA.CURRENT_DEPT.key] || item[ASSET_SCHEMA.CURRENT_DEPT.db]) === filters.dept;
|
||||
const matchLoc = !filters.loc || (item[ASSET_SCHEMA.LOCATION.key] || item[ASSET_SCHEMA.LOCATION.db]) === filters.loc;
|
||||
const matchField = !filters.field || (item[ASSET_SCHEMA.SW_FIELD.key] || item[ASSET_SCHEMA.SW_FIELD.db]) === filters.field;
|
||||
const matchType = !filters.type || (item[ASSET_SCHEMA.ASSET_TYPE.key] || item[ASSET_SCHEMA.ASSET_TYPE.db]) === filters.type;
|
||||
const matchStatus = !filters.status || (item[ASSET_SCHEMA.HW_STATUS.key] || item[ASSET_SCHEMA.HW_STATUS.db]) === filters.status;
|
||||
|
||||
return matchKeyword && matchCorp && matchDept && matchLoc && matchField && matchType && matchStatus;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,195 +1,195 @@
|
||||
/**
|
||||
* ITAM 통합 스키마 매퍼 (Unified Schema Mapper)
|
||||
*
|
||||
* key: 애플리케이션 내부 로직에서 사용하는 속성명
|
||||
* db: MySQL 데이터베이스 컬럼명
|
||||
* ui: 사용자에게 보여지는 UI 레이블
|
||||
*/
|
||||
|
||||
export const ASSET_SCHEMA = {
|
||||
// ─── 공통 필드 (Common) ───
|
||||
ID: { key: 'id', db: 'id', ui: 'ID' },
|
||||
ASSET_CODE: { key: 'asset_code', db: 'asset_code', ui: '자산번호' },
|
||||
CATEGORY: { key: 'category', db: 'category', ui: '구분' },
|
||||
ASSET_TYPE: { key: 'asset_type', db: 'asset_type', ui: '유형' },
|
||||
PURCHASE_CORP: { key: 'purchase_corp',db: 'purchase_corp', ui: '구매법인' },
|
||||
PURCHASE_DATE: { key: 'purchase_date',db: 'purchase_date', ui: '구매일자' },
|
||||
PURCHASE_AMOUNT:{ key: 'purchase_amount', db: 'purchase_amount', ui: '구매금액' },
|
||||
PURCHASE_VENDOR:{ key: 'purchase_vendor', db: 'purchase_vendor', ui: '구매업체' },
|
||||
APPROVAL_DOC: { key: 'approval_document', db: 'approval_document', ui: '품의서' },
|
||||
SERVICE_TYPE: { key: 'service_type', db: 'service_type', ui: '서비스 구분' },
|
||||
MANAGER_MAIN: { key: 'manager_primary', db: 'manager_primary', ui: '담당자(정)' },
|
||||
MANAGER_SUB: { key: 'manager_secondary', db: 'manager_secondary', ui: '담당자(부)' },
|
||||
LOCATION: { key: 'location', db: 'location', ui: '자산위치' },
|
||||
LOC_DETAIL: { key: 'location_detail', db: 'location_detail', ui: '상세위치' },
|
||||
LOCATION_PHOTO: { key: 'location_photo', db: 'location_photo', ui: '배치도이미지' },
|
||||
LOC_X: { key: 'loc_x', db: 'loc_x', ui: '위치X' },
|
||||
LOC_Y: { key: 'loc_y', db: 'loc_y', ui: '위치Y' },
|
||||
MEMO: { key: 'memo', db: 'memo', ui: '메모' },
|
||||
|
||||
// ─── 하드웨어 상세 (Hardware) ───
|
||||
HW_STATUS: { key: 'hw_status', db: 'hw_status', ui: '상태' },
|
||||
MODEL_NAME: { key: 'model_name', db: 'model_name', ui: '모델명' },
|
||||
ASSET_NAME: { key: 'asset_name', db: 'asset_name', ui: '자산명' },
|
||||
ASSET_MFR: { key: 'asset_mfr', db: 'asset_mfr', ui: '제조사' },
|
||||
CURRENT_DEPT: { key: 'current_dept', db: 'current_dept', ui: '현 사용조직' },
|
||||
PREV_DEPT: { key: 'previous_dept',db: 'previous_dept', ui: '직전 사용조직' },
|
||||
CURRENT_USER: { key: 'user_current', db: 'user_current', ui: '현 사용자' },
|
||||
EMP_NO: { key: 'emp_no', db: 'emp_no', ui: '사번' },
|
||||
USER_POSITION: { key: 'user_position', db: 'user_position', ui: '직무' },
|
||||
PREV_USER: { key: 'previous_user',db: 'previous_user', ui: '직전 사용자' },
|
||||
CPU: { key: 'cpu', db: 'cpu', ui: 'CPU' },
|
||||
RAM: { key: 'ram', db: 'ram', ui: 'RAM' },
|
||||
GPU: { key: 'gpu', db: 'gpu', ui: 'GPU' },
|
||||
SSD1: { key: 'ssd_1', db: 'ssd_1', ui: 'SSD1' },
|
||||
SSD2: { key: 'ssd_2', db: 'ssd_2', ui: 'SSD2' },
|
||||
HDD1: { key: 'hdd_1', db: 'hdd_1', ui: 'HDD1' },
|
||||
HDD2: { key: 'hdd_2', db: 'hdd_2', ui: 'HDD2' },
|
||||
HDD3: { key: 'hdd_3', db: 'hdd_3', ui: 'HDD3' },
|
||||
HDD4: { key: 'hdd_4', db: 'hdd_4', ui: 'HDD4' },
|
||||
MAINBOARD: { key: 'mainboard', db: 'mainboard', ui: '메인보드' },
|
||||
OS: { key: 'os', db: 'os', ui: 'OS' },
|
||||
IP_ADDR: { key: 'ip_address', db: 'ip_address', ui: 'IP 주소' },
|
||||
IP_ADDR2: { key: 'ip_address_2', db: 'ip_address_2', ui: 'IP 주소 2' },
|
||||
MAC_ADDR: { key: 'mac_address', db: 'mac_address', ui: 'MAC 주소' },
|
||||
REMOTE_TOOL: { key: 'remote_tool', db: 'remote_tool', ui: '원격도구' },
|
||||
REMOTE_ID: { key: 'remote_id', db: 'remote_id', ui: '원격 ID' },
|
||||
REMOTE_PW: { key: 'remote_pw', db: 'remote_pw', ui: '원격 PW' },
|
||||
MONITORING: { key: 'monitoring', db: 'monitoring', ui: '모니터링' },
|
||||
VOLUME: { key: 'volume', db: 'volume', ui: '용량' },
|
||||
MONITOR_INCH: { key: 'monitor_inch', db: 'monitor_inch', ui: '인치' },
|
||||
ASSET_COUNT: { key: 'asset_count', db: 'asset_count', ui: '수량' },
|
||||
SERIAL_NUM: { key: 'serial_num', db: 'serial_num', ui: 'S/N' },
|
||||
|
||||
// ─── 소프트웨어/클라우드 상세 (SW/Cloud/Domain) ───
|
||||
SW_STATUS: { key: 'sw_status', db: 'sw_status', ui: '상태' },
|
||||
SW_FIELD: { key: 'sw_field', db: 'sw_field', ui: '분야' },
|
||||
SW_TYPE: { key: 'sw_type', db: 'sw_type', ui: '유형' },
|
||||
DEV_OBJ: { key: 'dev_objective',db: 'dev_objective', ui: '목적' },
|
||||
DEV_MGR: { key: 'dev_manager', db: 'dev_manager', ui: '개발담당자' },
|
||||
PLANNING_MGR: { key: 'planning_manager', db: 'planning_manager', ui: '기획담당자' },
|
||||
SALES_MGR: { key: 'sales_manager',db: 'sales_manager', ui: '영업담당자' },
|
||||
PRODUCT_NAME: { key: 'product_name', db: 'product_name', ui: '제품명' },
|
||||
DOMAIN_ADDR: { key: 'domain_address', db: 'domain_address',ui: '도메인주소' },
|
||||
EMAIL_ACCOUNT: { key: 'email_account', db: 'email_account', ui: '이메일주소' },
|
||||
EMAIL_PW: { key: 'email_pw', db: 'email_pw', ui: '이메일비밀번호' },
|
||||
SW_ID: { key: 'sw_id', db: 'sw_id', ui: '계정ID' },
|
||||
SW_PW: { key: 'sw_pw', db: 'sw_pw', ui: '비밀번호' },
|
||||
PURCHASE_METHOD:{ key: 'purchase_method', db: 'purchase_method', ui: '결제수단' },
|
||||
ASSET_PURPOSE: { key: 'asset_purpose', db: 'asset_purpose', ui: '용도' },
|
||||
ASSET_STATUS: { key: 'asset_status', db: 'asset_status', ui: '상태' },
|
||||
START_DATE: { key: 'start_date', db: 'start_date', ui: '시작일' },
|
||||
EXPIRED_DATE: { key: 'expired_date', db: 'expired_date', ui: '만료일' }
|
||||
};
|
||||
|
||||
/**
|
||||
* 페이지별 헤더 정보 (타이틀, 설명, 아이콘)
|
||||
*/
|
||||
export const PAGE_DESCRIPTIONS: Record<string, { title: string; description: string; icon: string }> = {
|
||||
'PC': {
|
||||
title: '개인PC 자산 관리',
|
||||
description: '임직원에게 지급된 데스크톱 및 노트북 자산의 할당 현황과 하드웨어 사양을 통합 관리합니다.',
|
||||
icon: 'laptop'
|
||||
},
|
||||
'서버': {
|
||||
title: '서버 자산 관리',
|
||||
description: 'IDC 및 사내 서버실에 운영 중인 물리 서버 장비의 도입, 운영, 폐기 현황을 관리합니다.',
|
||||
icon: 'server'
|
||||
},
|
||||
'스토리지': {
|
||||
title: '스토리지 자산 관리',
|
||||
description: '데이터 저장 및 백업을 위한 NAS, DAS 등 스토리지 장비의 용량과 연결 상태를 관리합니다.',
|
||||
icon: 'database'
|
||||
},
|
||||
'네트워크': {
|
||||
title: '네트워크 장비 관리',
|
||||
description: '스위치, 방화벽, 공유기 등 사내 네트워크 인프라를 구성하는 주요 장비 현황을 관리합니다.',
|
||||
icon: 'layers'
|
||||
},
|
||||
'업무지원장비': {
|
||||
title: '업무 지원 장비 관리',
|
||||
description: '모니터, 프린터, 스캐너 등 원활한 업무 수행을 보조하는 전산 비품들을 관리합니다.',
|
||||
icon: 'monitor'
|
||||
},
|
||||
'PC부품': {
|
||||
title: 'PC 부품 자산 관리',
|
||||
description: 'CPU, RAM, GPU 등 PC 조립 및 유지보수를 위해 보유 중인 주요 부품 재고를 관리합니다.',
|
||||
icon: 'cpu'
|
||||
},
|
||||
'공간정보장비': {
|
||||
title: '공간 정보 장비 관리',
|
||||
description: '측량 및 공간 정보 수집에 사용되는 특수 정밀 장비들의 이력과 상태를 관리합니다.',
|
||||
icon: 'map'
|
||||
},
|
||||
'내부SW': {
|
||||
title: '사내 개발 S/W 관리',
|
||||
description: '사내에서 자체 개발하거나 운영 중인 시스템 및 소프트웨어 서비스 현황을 관리합니다.',
|
||||
icon: 'code'
|
||||
},
|
||||
'외부SW': {
|
||||
title: '외부 상용 S/W 관리',
|
||||
description: '상용 소프트웨어의 라이선스 보유 현황, 사용자 할당 및 만료 일정을 관리합니다.',
|
||||
icon: 'package'
|
||||
},
|
||||
'도메인': {
|
||||
title: '도메인 자산 관리',
|
||||
description: '운영 중인 서비스 도메인의 등록 정보, 관리 업체 및 갱신 만료일을 관리합니다.',
|
||||
icon: 'globe'
|
||||
},
|
||||
'클라우드': {
|
||||
title: '클라우드 자산 관리',
|
||||
description: 'AWS, Azure, GCP 등 클라우드 인프라 자원 및 구독 서비스 이용 현황을 관리합니다.',
|
||||
icon: 'cloud'
|
||||
},
|
||||
'비용관리': {
|
||||
title: 'IT 비용 집행 관리',
|
||||
description: '전산 자산 도입 및 유지보수에 소요되는 정기/비정기 지출 비용을 통합 관리합니다.',
|
||||
icon: 'credit-card'
|
||||
},
|
||||
'선물': {
|
||||
title: '내빈/외빈 선물 관리',
|
||||
description: '내외빈 방문 시 지급되는 기념품 및 선물용 자산의 재고와 지급 이력을 관리합니다.',
|
||||
icon: 'gift'
|
||||
},
|
||||
'사무가구': {
|
||||
title: '사무용 가구 관리',
|
||||
description: '책상, 의자, 캐비닛 등 사무 환경 구성을 위한 가구 자산의 배치 현황을 관리합니다.',
|
||||
icon: 'armchair'
|
||||
},
|
||||
'사용자': {
|
||||
title: '임직원 사용자 관리',
|
||||
description: 'IT 자산 할당 및 관리의 기준이 되는 사내 임직원(사용자) 정보를 데이터베이스 기반으로 직접 등록하고 수정합니다.',
|
||||
icon: 'users'
|
||||
},
|
||||
'부품 마스터': {
|
||||
title: '부품 표준 정보 관리',
|
||||
description: 'PC 사양 적정성 평가의 기준이 되는 부품 표준 정보 및 등급별 감점 점수를 관리합니다.',
|
||||
icon: 'cpu'
|
||||
},
|
||||
'직무별 기준 사양': {
|
||||
title: '직무별 기준 사양 관리',
|
||||
description: 'BIM 모델러, 개발자, 엔지니어 등 사내 직무별 권장 하드웨어 기준 및 성능 합격 점수를 관리합니다.',
|
||||
icon: 'sliders'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 용어 사전 (UI 텍스트 전용)
|
||||
*/
|
||||
export const UI_TEXT = {
|
||||
ACTION: {
|
||||
ADD: '신규 등록',
|
||||
EDIT: '수정',
|
||||
SAVE: '저장',
|
||||
DELETE: '삭제',
|
||||
CANCEL: '취소',
|
||||
CLOSE: '닫기',
|
||||
HISTORY_ADD: '이력 추가',
|
||||
RESET_FILTER: '필터 초기화'
|
||||
},
|
||||
MESSAGES: {
|
||||
CONFIRM_DELETE: '정말로 삭제하시겠습니까?',
|
||||
SAVE_SUCCESS: '성공적으로 저장되었습니다.',
|
||||
NO_DATA: '검색 결과가 없습니다.'
|
||||
}
|
||||
};
|
||||
/**
|
||||
* ITAM 통합 스키마 매퍼 (Unified Schema Mapper)
|
||||
*
|
||||
* key: 애플리케이션 내부 로직에서 사용하는 속성명
|
||||
* db: MySQL 데이터베이스 컬럼명
|
||||
* ui: 사용자에게 보여지는 UI 레이블
|
||||
*/
|
||||
|
||||
export const ASSET_SCHEMA = {
|
||||
// ─── 공통 필드 (Common) ───
|
||||
ID: { key: 'id', db: 'id', ui: 'ID' },
|
||||
ASSET_CODE: { key: 'asset_code', db: 'asset_code', ui: '자산번호' },
|
||||
CATEGORY: { key: 'category', db: 'category', ui: '구분' },
|
||||
ASSET_TYPE: { key: 'asset_type', db: 'asset_type', ui: '유형' },
|
||||
PURCHASE_CORP: { key: 'purchase_corp',db: 'purchase_corp', ui: '구매법인' },
|
||||
PURCHASE_DATE: { key: 'purchase_date',db: 'purchase_date', ui: '구매일자' },
|
||||
PURCHASE_AMOUNT:{ key: 'purchase_amount', db: 'purchase_amount', ui: '구매금액' },
|
||||
PURCHASE_VENDOR:{ key: 'purchase_vendor', db: 'purchase_vendor', ui: '구매업체' },
|
||||
APPROVAL_DOC: { key: 'approval_document', db: 'approval_document', ui: '품의서' },
|
||||
SERVICE_TYPE: { key: 'service_type', db: 'service_type', ui: '서비스 구분' },
|
||||
MANAGER_MAIN: { key: 'manager_primary', db: 'manager_primary', ui: '담당자(정)' },
|
||||
MANAGER_SUB: { key: 'manager_secondary', db: 'manager_secondary', ui: '담당자(부)' },
|
||||
LOCATION: { key: 'location', db: 'location', ui: '자산위치' },
|
||||
LOC_DETAIL: { key: 'location_detail', db: 'location_detail', ui: '상세위치' },
|
||||
LOCATION_PHOTO: { key: 'location_photo', db: 'location_photo', ui: '배치도이미지' },
|
||||
LOC_X: { key: 'loc_x', db: 'loc_x', ui: '위치X' },
|
||||
LOC_Y: { key: 'loc_y', db: 'loc_y', ui: '위치Y' },
|
||||
MEMO: { key: 'memo', db: 'memo', ui: '메모' },
|
||||
|
||||
// ─── 하드웨어 상세 (Hardware) ───
|
||||
HW_STATUS: { key: 'hw_status', db: 'hw_status', ui: '상태' },
|
||||
MODEL_NAME: { key: 'model_name', db: 'model_name', ui: '모델명' },
|
||||
ASSET_NAME: { key: 'asset_name', db: 'asset_name', ui: '자산명' },
|
||||
ASSET_MFR: { key: 'asset_mfr', db: 'asset_mfr', ui: '제조사' },
|
||||
CURRENT_DEPT: { key: 'current_dept', db: 'current_dept', ui: '현 사용조직' },
|
||||
PREV_DEPT: { key: 'previous_dept',db: 'previous_dept', ui: '직전 사용조직' },
|
||||
CURRENT_USER: { key: 'user_current', db: 'user_current', ui: '현 사용자' },
|
||||
EMP_NO: { key: 'emp_no', db: 'emp_no', ui: '사번' },
|
||||
USER_POSITION: { key: 'user_position', db: 'user_position', ui: '직무' },
|
||||
PREV_USER: { key: 'previous_user',db: 'previous_user', ui: '직전 사용자' },
|
||||
CPU: { key: 'cpu', db: 'cpu', ui: 'CPU' },
|
||||
RAM: { key: 'ram', db: 'ram', ui: 'RAM' },
|
||||
GPU: { key: 'gpu', db: 'gpu', ui: 'GPU' },
|
||||
SSD1: { key: 'ssd_1', db: 'ssd_1', ui: 'SSD1' },
|
||||
SSD2: { key: 'ssd_2', db: 'ssd_2', ui: 'SSD2' },
|
||||
HDD1: { key: 'hdd_1', db: 'hdd_1', ui: 'HDD1' },
|
||||
HDD2: { key: 'hdd_2', db: 'hdd_2', ui: 'HDD2' },
|
||||
HDD3: { key: 'hdd_3', db: 'hdd_3', ui: 'HDD3' },
|
||||
HDD4: { key: 'hdd_4', db: 'hdd_4', ui: 'HDD4' },
|
||||
MAINBOARD: { key: 'mainboard', db: 'mainboard', ui: '메인보드' },
|
||||
OS: { key: 'os', db: 'os', ui: 'OS' },
|
||||
IP_ADDR: { key: 'ip_address', db: 'ip_address', ui: 'IP 주소' },
|
||||
IP_ADDR2: { key: 'ip_address_2', db: 'ip_address_2', ui: 'IP 주소 2' },
|
||||
MAC_ADDR: { key: 'mac_address', db: 'mac_address', ui: 'MAC 주소' },
|
||||
REMOTE_TOOL: { key: 'remote_tool', db: 'remote_tool', ui: '원격도구' },
|
||||
REMOTE_ID: { key: 'remote_id', db: 'remote_id', ui: '원격 ID' },
|
||||
REMOTE_PW: { key: 'remote_pw', db: 'remote_pw', ui: '원격 PW' },
|
||||
MONITORING: { key: 'monitoring', db: 'monitoring', ui: '모니터링' },
|
||||
VOLUME: { key: 'volume', db: 'volume', ui: '용량' },
|
||||
MONITOR_INCH: { key: 'monitor_inch', db: 'monitor_inch', ui: '인치' },
|
||||
ASSET_COUNT: { key: 'asset_count', db: 'asset_count', ui: '수량' },
|
||||
SERIAL_NUM: { key: 'serial_num', db: 'serial_num', ui: 'S/N' },
|
||||
|
||||
// ─── 소프트웨어/클라우드 상세 (SW/Cloud/Domain) ───
|
||||
SW_STATUS: { key: 'sw_status', db: 'sw_status', ui: '상태' },
|
||||
SW_FIELD: { key: 'sw_field', db: 'sw_field', ui: '분야' },
|
||||
SW_TYPE: { key: 'sw_type', db: 'sw_type', ui: '유형' },
|
||||
DEV_OBJ: { key: 'dev_objective',db: 'dev_objective', ui: '목적' },
|
||||
DEV_MGR: { key: 'dev_manager', db: 'dev_manager', ui: '개발담당자' },
|
||||
PLANNING_MGR: { key: 'planning_manager', db: 'planning_manager', ui: '기획담당자' },
|
||||
SALES_MGR: { key: 'sales_manager',db: 'sales_manager', ui: '영업담당자' },
|
||||
PRODUCT_NAME: { key: 'product_name', db: 'product_name', ui: '제품명' },
|
||||
DOMAIN_ADDR: { key: 'domain_address', db: 'domain_address',ui: '도메인주소' },
|
||||
EMAIL_ACCOUNT: { key: 'email_account', db: 'email_account', ui: '이메일주소' },
|
||||
EMAIL_PW: { key: 'email_pw', db: 'email_pw', ui: '이메일비밀번호' },
|
||||
SW_ID: { key: 'sw_id', db: 'sw_id', ui: '계정ID' },
|
||||
SW_PW: { key: 'sw_pw', db: 'sw_pw', ui: '비밀번호' },
|
||||
PURCHASE_METHOD:{ key: 'purchase_method', db: 'purchase_method', ui: '결제수단' },
|
||||
ASSET_PURPOSE: { key: 'asset_purpose', db: 'asset_purpose', ui: '용도' },
|
||||
ASSET_STATUS: { key: 'asset_status', db: 'asset_status', ui: '상태' },
|
||||
START_DATE: { key: 'start_date', db: 'start_date', ui: '시작일' },
|
||||
EXPIRED_DATE: { key: 'expired_date', db: 'expired_date', ui: '만료일' }
|
||||
};
|
||||
|
||||
/**
|
||||
* 페이지별 헤더 정보 (타이틀, 설명, 아이콘)
|
||||
*/
|
||||
export const PAGE_DESCRIPTIONS: Record<string, { title: string; description: string; icon: string }> = {
|
||||
'PC': {
|
||||
title: '개인PC 자산 관리',
|
||||
description: '임직원에게 지급된 데스크톱 및 노트북 자산의 할당 현황과 하드웨어 사양을 통합 관리합니다.',
|
||||
icon: 'laptop'
|
||||
},
|
||||
'서버': {
|
||||
title: '서버 자산 관리',
|
||||
description: 'IDC 및 사내 서버실에 운영 중인 물리 서버 장비의 도입, 운영, 폐기 현황을 관리합니다.',
|
||||
icon: 'server'
|
||||
},
|
||||
'스토리지': {
|
||||
title: '스토리지 자산 관리',
|
||||
description: '데이터 저장 및 백업을 위한 NAS, DAS 등 스토리지 장비의 용량과 연결 상태를 관리합니다.',
|
||||
icon: 'database'
|
||||
},
|
||||
'네트워크': {
|
||||
title: '네트워크 장비 관리',
|
||||
description: '스위치, 방화벽, 공유기 등 사내 네트워크 인프라를 구성하는 주요 장비 현황을 관리합니다.',
|
||||
icon: 'layers'
|
||||
},
|
||||
'업무지원장비': {
|
||||
title: '업무 지원 장비 관리',
|
||||
description: '모니터, 프린터, 스캐너 등 원활한 업무 수행을 보조하는 전산 비품들을 관리합니다.',
|
||||
icon: 'monitor'
|
||||
},
|
||||
'PC부품': {
|
||||
title: 'PC 부품 자산 관리',
|
||||
description: 'CPU, RAM, GPU 등 PC 조립 및 유지보수를 위해 보유 중인 주요 부품 재고를 관리합니다.',
|
||||
icon: 'cpu'
|
||||
},
|
||||
'공간정보장비': {
|
||||
title: '공간 정보 장비 관리',
|
||||
description: '측량 및 공간 정보 수집에 사용되는 특수 정밀 장비들의 이력과 상태를 관리합니다.',
|
||||
icon: 'map'
|
||||
},
|
||||
'내부SW': {
|
||||
title: '사내 개발 S/W 관리',
|
||||
description: '사내에서 자체 개발하거나 운영 중인 시스템 및 소프트웨어 서비스 현황을 관리합니다.',
|
||||
icon: 'code'
|
||||
},
|
||||
'외부SW': {
|
||||
title: '외부 상용 S/W 관리',
|
||||
description: '상용 소프트웨어의 라이선스 보유 현황, 사용자 할당 및 만료 일정을 관리합니다.',
|
||||
icon: 'package'
|
||||
},
|
||||
'도메인': {
|
||||
title: '도메인 자산 관리',
|
||||
description: '운영 중인 서비스 도메인의 등록 정보, 관리 업체 및 갱신 만료일을 관리합니다.',
|
||||
icon: 'globe'
|
||||
},
|
||||
'클라우드': {
|
||||
title: '클라우드 자산 관리',
|
||||
description: 'AWS, Azure, GCP 등 클라우드 인프라 자원 및 구독 서비스 이용 현황을 관리합니다.',
|
||||
icon: 'cloud'
|
||||
},
|
||||
'비용관리': {
|
||||
title: 'IT 비용 집행 관리',
|
||||
description: '전산 자산 도입 및 유지보수에 소요되는 정기/비정기 지출 비용을 통합 관리합니다.',
|
||||
icon: 'credit-card'
|
||||
},
|
||||
'선물': {
|
||||
title: '내빈/외빈 선물 관리',
|
||||
description: '내외빈 방문 시 지급되는 기념품 및 선물용 자산의 재고와 지급 이력을 관리합니다.',
|
||||
icon: 'gift'
|
||||
},
|
||||
'사무가구': {
|
||||
title: '사무용 가구 관리',
|
||||
description: '책상, 의자, 캐비닛 등 사무 환경 구성을 위한 가구 자산의 배치 현황을 관리합니다.',
|
||||
icon: 'armchair'
|
||||
},
|
||||
'사용자': {
|
||||
title: '임직원 사용자 관리',
|
||||
description: 'IT 자산 할당 및 관리의 기준이 되는 사내 임직원(사용자) 정보를 데이터베이스 기반으로 직접 등록하고 수정합니다.',
|
||||
icon: 'users'
|
||||
},
|
||||
'부품 마스터': {
|
||||
title: '부품 표준 정보 관리',
|
||||
description: 'PC 사양 적정성 평가의 기준이 되는 부품 표준 정보 및 등급별 감점 점수를 관리합니다.',
|
||||
icon: 'cpu'
|
||||
},
|
||||
'직무별 기준 사양': {
|
||||
title: '직무별 기준 사양 관리',
|
||||
description: 'BIM 모델러, 개발자, 엔지니어 등 사내 직무별 권장 하드웨어 기준 및 성능 합격 점수를 관리합니다.',
|
||||
icon: 'sliders'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 용어 사전 (UI 텍스트 전용)
|
||||
*/
|
||||
export const UI_TEXT = {
|
||||
ACTION: {
|
||||
ADD: '신규 등록',
|
||||
EDIT: '수정',
|
||||
SAVE: '저장',
|
||||
DELETE: '삭제',
|
||||
CANCEL: '취소',
|
||||
CLOSE: '닫기',
|
||||
HISTORY_ADD: '이력 추가',
|
||||
RESET_FILTER: '필터 초기화'
|
||||
},
|
||||
MESSAGES: {
|
||||
CONFIRM_DELETE: '정말로 삭제하시겠습니까?',
|
||||
SAVE_SUCCESS: '성공적으로 저장되었습니다.',
|
||||
NO_DATA: '검색 결과가 없습니다.'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,293 +1,293 @@
|
||||
import { HardwareAsset, SoftwareAsset, SWUser, HardwareLog, MasterAssetData, SystemUser } from './types';
|
||||
import { API_BASE_URL } from './utils';
|
||||
import { HardwareAsset, SoftwareAsset, SWUser, HardwareLog, MasterAssetData, SystemUser } from './types';
|
||||
import { API_BASE_URL } from './utils';
|
||||
|
||||
// --- State Definitions ---
|
||||
export interface AppState {
|
||||
activeCategory: 'dashboard' | 'hw' | 'sw' | 'ops' | 'vip' | 'fac' | 'users' | 'etc';
|
||||
activeSubTab: string;
|
||||
viewMode: 'location' | 'legacy' | 'list';
|
||||
masterData: MasterAssetData;
|
||||
activeCharts: any[];
|
||||
currentUserRole: 'admin' | 'user';
|
||||
listFilters?: Record<string, any>;
|
||||
}
|
||||
|
||||
// 초기 상태
|
||||
export const state: AppState = {
|
||||
activeCategory: 'hw',
|
||||
activeSubTab: '대시보드',
|
||||
viewMode: 'location',
|
||||
activeCharts: [],
|
||||
currentUserRole: 'user',
|
||||
listFilters: {},
|
||||
masterData: {
|
||||
users: [],
|
||||
pc: [], server: [], storage: [], network: [],
|
||||
survey: [], pcParts: [], partsMaster: [], equipment: [], officeSupplies: [],
|
||||
swInternal: [], swExternal: [], cloud: [], domain: [],
|
||||
cost: [], vip: [],
|
||||
hw: [], sw: [],
|
||||
swUsers: [], logs: [],
|
||||
jobSpecs: [],
|
||||
mobile: []
|
||||
}
|
||||
};
|
||||
|
||||
(window as any).__itam_state = state;
|
||||
|
||||
/**
|
||||
* 통합 V2 스키마에 맞춘 데이터 로드
|
||||
*/
|
||||
export async function loadMasterDataFromDB() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/assets/master`);
|
||||
if (!response.ok) throw new Error('Failed to fetch master data');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// DB의 쪼개진 asset_remote 데이터로부터 가상 대표 속성(IP, MAC, 원격도구)을 주입해주는 전처리 함수
|
||||
const preprocessAssets = (assets: any[]) => {
|
||||
if (!Array.isArray(assets)) return;
|
||||
assets.forEach((asset: any) => {
|
||||
let ip = '';
|
||||
let mac = '';
|
||||
let remoteTool = '';
|
||||
let remoteId = '';
|
||||
// --- State Definitions ---
|
||||
export interface AppState {
|
||||
activeCategory: 'dashboard' | 'hw' | 'sw' | 'ops' | 'vip' | 'fac' | 'users' | 'etc';
|
||||
activeSubTab: string;
|
||||
viewMode: 'location' | 'legacy' | 'list';
|
||||
masterData: MasterAssetData;
|
||||
activeCharts: any[];
|
||||
currentUserRole: 'admin' | 'user';
|
||||
listFilters?: Record<string, any>;
|
||||
}
|
||||
|
||||
// 초기 상태
|
||||
export const state: AppState = {
|
||||
activeCategory: 'hw',
|
||||
activeSubTab: '대시보드',
|
||||
viewMode: 'location',
|
||||
activeCharts: [],
|
||||
currentUserRole: 'user',
|
||||
listFilters: {},
|
||||
masterData: {
|
||||
users: [],
|
||||
pc: [], server: [], storage: [], network: [],
|
||||
survey: [], pcParts: [], partsMaster: [], equipment: [], officeSupplies: [],
|
||||
swInternal: [], swExternal: [], cloud: [], domain: [],
|
||||
cost: [], vip: [],
|
||||
hw: [], sw: [],
|
||||
swUsers: [], logs: [],
|
||||
jobSpecs: [],
|
||||
mobile: []
|
||||
}
|
||||
};
|
||||
|
||||
(window as any).__itam_state = state;
|
||||
|
||||
/**
|
||||
* 통합 V2 스키마에 맞춘 데이터 로드
|
||||
*/
|
||||
export async function loadMasterDataFromDB() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/assets/master`);
|
||||
if (!response.ok) throw new Error('Failed to fetch master data');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// DB의 쪼개진 asset_remote 데이터로부터 가상 대표 속성(IP, MAC, 원격도구)을 주입해주는 전처리 함수
|
||||
const preprocessAssets = (assets: any[]) => {
|
||||
if (!Array.isArray(assets)) return;
|
||||
assets.forEach((asset: any) => {
|
||||
let ip = '';
|
||||
let mac = '';
|
||||
let remoteTool = '';
|
||||
let remoteId = '';
|
||||
let remotePw = '';
|
||||
|
||||
let rems: any[] = [];
|
||||
try {
|
||||
rems = asset.remotes ? (typeof asset.remotes === 'string' ? JSON.parse(asset.remotes) : asset.remotes) : [];
|
||||
} catch(e) {}
|
||||
|
||||
if (Array.isArray(rems)) {
|
||||
rems.forEach((r: any) => {
|
||||
if (r.type === 'IP') {
|
||||
if (!ip) ip = r.val1 || '';
|
||||
if (r.val2) {
|
||||
if (String(r.val2).trim().startsWith('{')) {
|
||||
try {
|
||||
const parsed = JSON.parse(r.val2);
|
||||
remoteTool = r.name || '원격접속';
|
||||
remoteId = parsed.id || '';
|
||||
remotePw = parsed.pw || '';
|
||||
} catch(e) {}
|
||||
} else {
|
||||
if (!mac) mac = r.val2 || '';
|
||||
}
|
||||
}
|
||||
} else if (r.type === 'MAC') {
|
||||
if (!mac) mac = r.val1 || '';
|
||||
} else if (r.type === 'REMOTE') {
|
||||
if (!remoteTool) remoteTool = r.name || '';
|
||||
if (!remoteId) remoteId = r.val1 || '';
|
||||
if (!remotePw) remotePw = r.val2 || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 최상위 가상 속성 바인딩 (목록 및 위치보기 뷰어 매핑용)
|
||||
asset.ip_address = ip;
|
||||
asset.mac_address = mac;
|
||||
asset.remote_tool = remoteTool;
|
||||
asset.remote_id = remoteId;
|
||||
asset.remote_pw = remotePw;
|
||||
});
|
||||
};
|
||||
|
||||
if (data) {
|
||||
const keys = ['pc', 'server', 'storage', 'network', 'survey', 'equipment', 'officeSupplies'];
|
||||
keys.forEach(k => {
|
||||
if (data[k]) preprocessAssets(data[k]);
|
||||
});
|
||||
}
|
||||
|
||||
// 전역 상태 업데이트
|
||||
state.masterData = {
|
||||
...state.masterData,
|
||||
...data,
|
||||
jobSpecs: data.jobSpecs || [],
|
||||
logs: (data.logs || []).map((l: any) => ({
|
||||
...l,
|
||||
assetId: l.asset_id || l.assetId,
|
||||
date: l.log_date || l.date,
|
||||
user: l.log_user || l.user,
|
||||
log_date: l.log_date || l.date,
|
||||
log_user: l.log_user || l.user
|
||||
}))
|
||||
};
|
||||
|
||||
// Mapping for backward compatibility
|
||||
(state.masterData as any).equip = state.masterData.equipment;
|
||||
(state.masterData as any).subSw = state.masterData.swExternal;
|
||||
(state.masterData as any).permSw = state.masterData.swInternal;
|
||||
|
||||
// 하드웨어 통합 (대시보드 호환용)
|
||||
state.masterData.hw = [
|
||||
...state.masterData.pc,
|
||||
...state.masterData.server,
|
||||
...state.masterData.storage,
|
||||
...state.masterData.network,
|
||||
...state.masterData.survey,
|
||||
...state.masterData.equipment,
|
||||
...state.masterData.officeSupplies
|
||||
];
|
||||
|
||||
// 소프트웨어 통합
|
||||
state.masterData.sw = [
|
||||
...state.masterData.swInternal,
|
||||
...state.masterData.swExternal,
|
||||
...(state.masterData.cloud || [])
|
||||
];
|
||||
|
||||
console.log('✅ V2 Normalized data loaded successfully');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('⚠️ Dummy 로드 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function updateState(newState: Partial<AppState>) {
|
||||
Object.assign(state, newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 자산 저장 (V2 Normalized API)
|
||||
*/
|
||||
export async function saveAsset(category: string, asset: any) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/asset/${category}/save`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(asset)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('자산 저장 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 자산 삭제 (V2 API)
|
||||
*/
|
||||
export async function deleteAsset(category: string, assetId: string) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/asset/${category}/${assetId}`;
|
||||
const response = await fetch(url, { method: 'DELETE' });
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('자산 삭제 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function savePartsMaster(component: any) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/hardware-components/save`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(component)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('부품 마스터 저장 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function deletePartsMaster(id: number) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/hardware-components/${id}`;
|
||||
const response = await fetch(url, { method: 'DELETE' });
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('부품 마스터 삭제 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function saveSystemUser(user: any) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/system-users/save`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(user)
|
||||
});
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('사용자 정보 저장 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function deleteSystemUser(id: string) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/system-users/${id}`;
|
||||
const response = await fetch(url, { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('사용자 정보 삭제 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function saveJobSpec(spec: any) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/job-specs/save`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(spec)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('직무별 기준 사양 저장 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function deleteJobSpec(id: number) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/job-specs/${id}`;
|
||||
const response = await fetch(url, { method: 'DELETE' });
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('직무별 기준 사양 삭제 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let rems: any[] = [];
|
||||
try {
|
||||
rems = asset.remotes ? (typeof asset.remotes === 'string' ? JSON.parse(asset.remotes) : asset.remotes) : [];
|
||||
} catch(e) {}
|
||||
|
||||
if (Array.isArray(rems)) {
|
||||
rems.forEach((r: any) => {
|
||||
if (r.type === 'IP') {
|
||||
if (!ip) ip = r.val1 || '';
|
||||
if (r.val2) {
|
||||
if (String(r.val2).trim().startsWith('{')) {
|
||||
try {
|
||||
const parsed = JSON.parse(r.val2);
|
||||
remoteTool = r.name || '원격접속';
|
||||
remoteId = parsed.id || '';
|
||||
remotePw = parsed.pw || '';
|
||||
} catch(e) {}
|
||||
} else {
|
||||
if (!mac) mac = r.val2 || '';
|
||||
}
|
||||
}
|
||||
} else if (r.type === 'MAC') {
|
||||
if (!mac) mac = r.val1 || '';
|
||||
} else if (r.type === 'REMOTE') {
|
||||
if (!remoteTool) remoteTool = r.name || '';
|
||||
if (!remoteId) remoteId = r.val1 || '';
|
||||
if (!remotePw) remotePw = r.val2 || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 최상위 가상 속성 바인딩 (목록 및 위치보기 뷰어 매핑용)
|
||||
asset.ip_address = ip;
|
||||
asset.mac_address = mac;
|
||||
asset.remote_tool = remoteTool;
|
||||
asset.remote_id = remoteId;
|
||||
asset.remote_pw = remotePw;
|
||||
});
|
||||
};
|
||||
|
||||
if (data) {
|
||||
const keys = ['pc', 'server', 'storage', 'network', 'survey', 'equipment', 'officeSupplies'];
|
||||
keys.forEach(k => {
|
||||
if (data[k]) preprocessAssets(data[k]);
|
||||
});
|
||||
}
|
||||
|
||||
// 전역 상태 업데이트
|
||||
state.masterData = {
|
||||
...state.masterData,
|
||||
...data,
|
||||
jobSpecs: data.jobSpecs || [],
|
||||
logs: (data.logs || []).map((l: any) => ({
|
||||
...l,
|
||||
assetId: l.asset_id || l.assetId,
|
||||
date: l.log_date || l.date,
|
||||
user: l.log_user || l.user,
|
||||
log_date: l.log_date || l.date,
|
||||
log_user: l.log_user || l.user
|
||||
}))
|
||||
};
|
||||
|
||||
// Mapping for backward compatibility
|
||||
(state.masterData as any).equip = state.masterData.equipment;
|
||||
(state.masterData as any).subSw = state.masterData.swExternal;
|
||||
(state.masterData as any).permSw = state.masterData.swInternal;
|
||||
|
||||
// 하드웨어 통합 (대시보드 호환용)
|
||||
state.masterData.hw = [
|
||||
...state.masterData.pc,
|
||||
...state.masterData.server,
|
||||
...state.masterData.storage,
|
||||
...state.masterData.network,
|
||||
...state.masterData.survey,
|
||||
...state.masterData.equipment,
|
||||
...state.masterData.officeSupplies
|
||||
];
|
||||
|
||||
// 소프트웨어 통합
|
||||
state.masterData.sw = [
|
||||
...state.masterData.swInternal,
|
||||
...state.masterData.swExternal,
|
||||
...(state.masterData.cloud || [])
|
||||
];
|
||||
|
||||
console.log('✅ V2 Normalized data loaded successfully');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('⚠️ Dummy 로드 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function updateState(newState: Partial<AppState>) {
|
||||
Object.assign(state, newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 자산 저장 (V2 Normalized API)
|
||||
*/
|
||||
export async function saveAsset(category: string, asset: any) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/asset/${category}/save`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(asset)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('자산 저장 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 자산 삭제 (V2 API)
|
||||
*/
|
||||
export async function deleteAsset(category: string, assetId: string) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/asset/${category}/${assetId}`;
|
||||
const response = await fetch(url, { method: 'DELETE' });
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('자산 삭제 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function savePartsMaster(component: any) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/hardware-components/save`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(component)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('부품 마스터 저장 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function deletePartsMaster(id: number) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/hardware-components/${id}`;
|
||||
const response = await fetch(url, { method: 'DELETE' });
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('부품 마스터 삭제 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function saveSystemUser(user: any) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/system-users/save`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(user)
|
||||
});
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('사용자 정보 저장 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function deleteSystemUser(id: string) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/system-users/${id}`;
|
||||
const response = await fetch(url, { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('사용자 정보 삭제 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function saveJobSpec(spec: any) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/job-specs/save`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(spec)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('직무별 기준 사양 저장 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function deleteJobSpec(id: number) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/job-specs/${id}`;
|
||||
const response = await fetch(url, { method: 'DELETE' });
|
||||
|
||||
if (response.ok) {
|
||||
await loadMasterDataFromDB(); // 전역 상태 갱신
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('직무별 기준 사양 삭제 실패:', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
/**
|
||||
* 공통 테이블 핸들러
|
||||
*/
|
||||
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export interface SortState {
|
||||
key: string;
|
||||
direction: SortDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 헤더에 정렬 이벤트를 바인딩합니다.
|
||||
* @param table 대상 테이블 요소
|
||||
* @param currentState 현재 정렬 상태
|
||||
* @param onSort 정렬 변경 시 호출될 콜백
|
||||
*/
|
||||
export function setupTableSorting(
|
||||
table: HTMLTableElement,
|
||||
currentState: SortState,
|
||||
onSort: (key: string, direction: SortDirection) => void
|
||||
) {
|
||||
const headers = table.querySelectorAll('th[data-sort]');
|
||||
|
||||
headers.forEach(th => {
|
||||
const key = th.getAttribute('data-sort')!;
|
||||
th.classList.add('sortable');
|
||||
|
||||
// 현재 정렬 상태 표시
|
||||
if (currentState.key === key) {
|
||||
th.classList.add(currentState.direction);
|
||||
} else {
|
||||
th.classList.remove('asc', 'desc');
|
||||
}
|
||||
|
||||
(th as HTMLElement).onclick = () => {
|
||||
let nextDirection: SortDirection = 'asc';
|
||||
|
||||
if (currentState.key === key) {
|
||||
nextDirection = currentState.direction === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
onSort(key, nextDirection);
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 공통 테이블 핸들러
|
||||
*/
|
||||
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export interface SortState {
|
||||
key: string;
|
||||
direction: SortDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 헤더에 정렬 이벤트를 바인딩합니다.
|
||||
* @param table 대상 테이블 요소
|
||||
* @param currentState 현재 정렬 상태
|
||||
* @param onSort 정렬 변경 시 호출될 콜백
|
||||
*/
|
||||
export function setupTableSorting(
|
||||
table: HTMLTableElement,
|
||||
currentState: SortState,
|
||||
onSort: (key: string, direction: SortDirection) => void
|
||||
) {
|
||||
const headers = table.querySelectorAll('th[data-sort]');
|
||||
|
||||
headers.forEach(th => {
|
||||
const key = th.getAttribute('data-sort')!;
|
||||
th.classList.add('sortable');
|
||||
|
||||
// 현재 정렬 상태 표시
|
||||
if (currentState.key === key) {
|
||||
th.classList.add(currentState.direction);
|
||||
} else {
|
||||
th.classList.remove('asc', 'desc');
|
||||
}
|
||||
|
||||
(th as HTMLElement).onclick = () => {
|
||||
let nextDirection: SortDirection = 'asc';
|
||||
|
||||
if (currentState.key === key) {
|
||||
nextDirection = currentState.direction === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
onSort(key, nextDirection);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,155 +1,155 @@
|
||||
/**
|
||||
* ITAM Global Type Definitions
|
||||
*/
|
||||
|
||||
export interface BaseAsset {
|
||||
id: string;
|
||||
asset_code?: string;
|
||||
category?: string;
|
||||
asset_type?: string;
|
||||
purchase_corp?: string;
|
||||
purchase_date?: string;
|
||||
purchase_amount?: number | string;
|
||||
purchase_vendor?: string;
|
||||
approval_document?: string;
|
||||
service_type?: string;
|
||||
manager_primary?: string;
|
||||
manager_secondary?: string;
|
||||
location?: string;
|
||||
location_detail?: string;
|
||||
location_photo?: string;
|
||||
loc_x?: number;
|
||||
loc_y?: number;
|
||||
memo?: string;
|
||||
updated_at?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface HardwareAsset extends BaseAsset {
|
||||
hw_status?: string;
|
||||
model_name?: string;
|
||||
asset_name?: string;
|
||||
asset_mfr?: string;
|
||||
current_dept?: string;
|
||||
previous_dept?: string;
|
||||
user_current?: string;
|
||||
emp_no?: string;
|
||||
user_position?: string;
|
||||
previous_user?: string;
|
||||
cpu?: string;
|
||||
ram?: string;
|
||||
gpu?: string;
|
||||
ssd_1?: string;
|
||||
ssd_2?: string;
|
||||
hdd_1?: string;
|
||||
hdd_2?: string;
|
||||
hdd_3?: string;
|
||||
hdd_4?: string;
|
||||
mainboard?: string;
|
||||
os?: string;
|
||||
ip_address?: string;
|
||||
ip_address_2?: string;
|
||||
mac_address?: string;
|
||||
remote_tool?: string;
|
||||
remote_id?: string;
|
||||
remote_pw?: string;
|
||||
monitoring?: string;
|
||||
volume?: string;
|
||||
monitor_inch?: string;
|
||||
asset_count?: number | string;
|
||||
serial_num?: string;
|
||||
// Normalized V3 fields
|
||||
volumes?: any[];
|
||||
remotes?: any[];
|
||||
}
|
||||
|
||||
export interface SoftwareAsset extends BaseAsset {
|
||||
sw_status?: string;
|
||||
sw_field?: string;
|
||||
sw_type?: string;
|
||||
dev_objective?: string;
|
||||
dev_manager?: string;
|
||||
planning_manager?: string;
|
||||
sales_manager?: string;
|
||||
product_name?: string;
|
||||
domain_address?: string;
|
||||
email_account?: string;
|
||||
email_pw?: string;
|
||||
sw_id?: string;
|
||||
sw_pw?: string;
|
||||
purchase_method?: string;
|
||||
asset_purpose?: string;
|
||||
asset_status?: string;
|
||||
start_date?: string;
|
||||
expired_date?: string;
|
||||
}
|
||||
|
||||
export interface SWUser {
|
||||
id: string;
|
||||
sw_id: string;
|
||||
user_name: string;
|
||||
dept: string;
|
||||
corp: string;
|
||||
emp_no?: string;
|
||||
created_at?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface HardwareLog {
|
||||
id: string;
|
||||
asset_id: string;
|
||||
log_date: string;
|
||||
log_user: string;
|
||||
event_type: string;
|
||||
details: string;
|
||||
old_dept?: string;
|
||||
new_dept?: string;
|
||||
old_user?: string;
|
||||
new_user?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface SystemUser {
|
||||
id: string;
|
||||
emp_no: string;
|
||||
user_name: string;
|
||||
dept_name: string;
|
||||
position: string;
|
||||
status: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface PartsMaster {
|
||||
id: number | string;
|
||||
category: string;
|
||||
component_name: string;
|
||||
score_tier: string;
|
||||
deduction: number;
|
||||
}
|
||||
|
||||
export interface MasterAssetData {
|
||||
users: SystemUser[];
|
||||
pc: HardwareAsset[];
|
||||
server: HardwareAsset[];
|
||||
storage: HardwareAsset[];
|
||||
network: HardwareAsset[];
|
||||
survey: HardwareAsset[];
|
||||
pcParts: HardwareAsset[];
|
||||
partsMaster: PartsMaster[];
|
||||
equipment: HardwareAsset[];
|
||||
officeSupplies: HardwareAsset[];
|
||||
swInternal: SoftwareAsset[];
|
||||
swExternal: SoftwareAsset[];
|
||||
cloud: SoftwareAsset[];
|
||||
domain: SoftwareAsset[];
|
||||
cost: any[];
|
||||
vip: HardwareAsset[];
|
||||
swUsers: SWUser[];
|
||||
logs: HardwareLog[];
|
||||
jobSpecs?: any[];
|
||||
mobile?: HardwareAsset[];
|
||||
// Integrated arrays
|
||||
hw: HardwareAsset[];
|
||||
sw: SoftwareAsset[];
|
||||
}
|
||||
/**
|
||||
* ITAM Global Type Definitions
|
||||
*/
|
||||
|
||||
export interface BaseAsset {
|
||||
id: string;
|
||||
asset_code?: string;
|
||||
category?: string;
|
||||
asset_type?: string;
|
||||
purchase_corp?: string;
|
||||
purchase_date?: string;
|
||||
purchase_amount?: number | string;
|
||||
purchase_vendor?: string;
|
||||
approval_document?: string;
|
||||
service_type?: string;
|
||||
manager_primary?: string;
|
||||
manager_secondary?: string;
|
||||
location?: string;
|
||||
location_detail?: string;
|
||||
location_photo?: string;
|
||||
loc_x?: number;
|
||||
loc_y?: number;
|
||||
memo?: string;
|
||||
updated_at?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface HardwareAsset extends BaseAsset {
|
||||
hw_status?: string;
|
||||
model_name?: string;
|
||||
asset_name?: string;
|
||||
asset_mfr?: string;
|
||||
current_dept?: string;
|
||||
previous_dept?: string;
|
||||
user_current?: string;
|
||||
emp_no?: string;
|
||||
user_position?: string;
|
||||
previous_user?: string;
|
||||
cpu?: string;
|
||||
ram?: string;
|
||||
gpu?: string;
|
||||
ssd_1?: string;
|
||||
ssd_2?: string;
|
||||
hdd_1?: string;
|
||||
hdd_2?: string;
|
||||
hdd_3?: string;
|
||||
hdd_4?: string;
|
||||
mainboard?: string;
|
||||
os?: string;
|
||||
ip_address?: string;
|
||||
ip_address_2?: string;
|
||||
mac_address?: string;
|
||||
remote_tool?: string;
|
||||
remote_id?: string;
|
||||
remote_pw?: string;
|
||||
monitoring?: string;
|
||||
volume?: string;
|
||||
monitor_inch?: string;
|
||||
asset_count?: number | string;
|
||||
serial_num?: string;
|
||||
// Normalized V3 fields
|
||||
volumes?: any[];
|
||||
remotes?: any[];
|
||||
}
|
||||
|
||||
export interface SoftwareAsset extends BaseAsset {
|
||||
sw_status?: string;
|
||||
sw_field?: string;
|
||||
sw_type?: string;
|
||||
dev_objective?: string;
|
||||
dev_manager?: string;
|
||||
planning_manager?: string;
|
||||
sales_manager?: string;
|
||||
product_name?: string;
|
||||
domain_address?: string;
|
||||
email_account?: string;
|
||||
email_pw?: string;
|
||||
sw_id?: string;
|
||||
sw_pw?: string;
|
||||
purchase_method?: string;
|
||||
asset_purpose?: string;
|
||||
asset_status?: string;
|
||||
start_date?: string;
|
||||
expired_date?: string;
|
||||
}
|
||||
|
||||
export interface SWUser {
|
||||
id: string;
|
||||
sw_id: string;
|
||||
user_name: string;
|
||||
dept: string;
|
||||
corp: string;
|
||||
emp_no?: string;
|
||||
created_at?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface HardwareLog {
|
||||
id: string;
|
||||
asset_id: string;
|
||||
log_date: string;
|
||||
log_user: string;
|
||||
event_type: string;
|
||||
details: string;
|
||||
old_dept?: string;
|
||||
new_dept?: string;
|
||||
old_user?: string;
|
||||
new_user?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface SystemUser {
|
||||
id: string;
|
||||
emp_no: string;
|
||||
user_name: string;
|
||||
dept_name: string;
|
||||
position: string;
|
||||
status: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface PartsMaster {
|
||||
id: number | string;
|
||||
category: string;
|
||||
component_name: string;
|
||||
score_tier: string;
|
||||
deduction: number;
|
||||
}
|
||||
|
||||
export interface MasterAssetData {
|
||||
users: SystemUser[];
|
||||
pc: HardwareAsset[];
|
||||
server: HardwareAsset[];
|
||||
storage: HardwareAsset[];
|
||||
network: HardwareAsset[];
|
||||
survey: HardwareAsset[];
|
||||
pcParts: HardwareAsset[];
|
||||
partsMaster: PartsMaster[];
|
||||
equipment: HardwareAsset[];
|
||||
officeSupplies: HardwareAsset[];
|
||||
swInternal: SoftwareAsset[];
|
||||
swExternal: SoftwareAsset[];
|
||||
cloud: SoftwareAsset[];
|
||||
domain: SoftwareAsset[];
|
||||
cost: any[];
|
||||
vip: HardwareAsset[];
|
||||
swUsers: SWUser[];
|
||||
logs: HardwareLog[];
|
||||
jobSpecs?: any[];
|
||||
mobile?: HardwareAsset[];
|
||||
// Integrated arrays
|
||||
hw: HardwareAsset[];
|
||||
sw: SoftwareAsset[];
|
||||
}
|
||||
|
||||
@@ -1,359 +1,359 @@
|
||||
import { PAGE_DESCRIPTIONS } from './schema';
|
||||
|
||||
export const API_BASE_URL = '';
|
||||
|
||||
/**
|
||||
* ITAM 공통 유틸리티 함수
|
||||
*/
|
||||
|
||||
/**
|
||||
* 페이지 헤더(타이틀 및 설명) 렌더링
|
||||
*/
|
||||
export function renderPageHeader(container: HTMLElement, pageId: string) {
|
||||
const config = PAGE_DESCRIPTIONS[pageId];
|
||||
if (!config) return;
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'page-header';
|
||||
header.innerHTML = `
|
||||
<div class="page-title-group">
|
||||
<h2 class="page-title">${config.title}</h2>
|
||||
<p class="page-description">${config.description}</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 숫자에 천 단위 콤마 추가 (금액 표시용)
|
||||
*/
|
||||
export function formatPrice(value: string | number): string {
|
||||
if (value === undefined || value === null) return '';
|
||||
const num = String(value).replace(/[^0-9]/g, '');
|
||||
if (!num) return '';
|
||||
return num.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 배지 생성 (정/부 담당자, 원격도구 등)
|
||||
*/
|
||||
export function createBadge(text: string, type: 'primary' | 'muted' | 'success' | 'danger' = 'primary'): string {
|
||||
return `<span class="badge badge-${type}">${text}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트 내 줄바꿈을 구분자(/)로 변경하여 한 줄로 표시
|
||||
*/
|
||||
export function formatInline(value: any): string {
|
||||
return String(value || '').replace(/\n/g, ' / ').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 문자열 포맷팅 (YYYY.MM.DD -> YYYY-MM-DD)
|
||||
*/
|
||||
export function normalizeDate(dateStr: string): string {
|
||||
if (!dateStr) return '';
|
||||
let str = String(dateStr).replace(/\./g, '-').trim();
|
||||
// YYYYMM 형식 처리 (6자리 숫자)
|
||||
if (/^\d{6}$/.test(str)) {
|
||||
return `${str.substring(0, 4)}-${str.substring(4, 6)}`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 구매일로부터 현재까지의 경과 연수 계산 (소수점 첫째자리)
|
||||
*/
|
||||
export function calculateAssetAge(purchaseDate: string): number {
|
||||
const normalized = normalizeDate(purchaseDate);
|
||||
if (!normalized) return 0;
|
||||
|
||||
const purchase = new Date(normalized);
|
||||
if (isNaN(purchase.getTime())) return 0;
|
||||
|
||||
const diffMs = Date.now() - purchase.getTime();
|
||||
const age = diffMs / (1000 * 60 * 60 * 24 * 365.25);
|
||||
return Math.max(0, parseFloat(age.toFixed(1)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 고유 ID 생성 (7자리 랜덤 문자열)
|
||||
*/
|
||||
export function generateId(): string {
|
||||
return Math.random().toString(36).substring(2, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 자산 객체 간의 변경 사항 감지
|
||||
*/
|
||||
export function getAssetChanges(oldAsset: any, newAsset: any, fields: {key: string, label: string}[]): string {
|
||||
const changes: string[] = [];
|
||||
fields.forEach(field => {
|
||||
const oldVal = String(oldAsset[field.key] || '').trim();
|
||||
const newVal = String(newAsset[field.key] || '').trim();
|
||||
if (oldVal !== newVal) {
|
||||
changes.push(`${field.label}: ${oldVal || '없음'} → ${newVal || '없음'}`);
|
||||
}
|
||||
});
|
||||
return changes.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 자산 목록 정렬 (기본: 법인별 -> 자산번호 순)
|
||||
*/
|
||||
export function sortAssets<T>(list: T[]): T[] {
|
||||
return [...list].sort((a: any, b: any) => {
|
||||
// 1순위: 법인 (가나다순)
|
||||
const corpA = String(a.법인 || a.corp || '').trim();
|
||||
const corpB = String(b.법인 || b.corp || '').trim();
|
||||
if (corpA < corpB) return -1;
|
||||
if (corpA > corpB) return 1;
|
||||
|
||||
// 2순위: 자산번호/코드 (영문/숫자순)
|
||||
const codeA = String(a.자산코드 || a.자산번호 || a.id || '').trim();
|
||||
const codeB = String(b.자산코드 || b.자산번호 || b.id || '').trim();
|
||||
if (codeA < codeB) return -1;
|
||||
if (codeA > codeB) return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 동적 정렬 함수
|
||||
* @param list 정렬할 목록
|
||||
* @param key 정렬 기준 필드
|
||||
* @param direction 정렬 방향 ('asc' | 'desc')
|
||||
*/
|
||||
export function dynamicSort<T>(list: T[], key: string, direction: 'asc' | 'desc'): T[] {
|
||||
return [...list].sort((a: any, b: any) => {
|
||||
let valA = a[key];
|
||||
let valB = b[key];
|
||||
|
||||
// 숫자인 경우 처리
|
||||
if (typeof valA === 'number' && typeof valB === 'number') {
|
||||
return direction === 'asc' ? valA - valB : valB - valA;
|
||||
}
|
||||
|
||||
// 금액 필드 (숫자형 문자열 포함) 처리
|
||||
if (key === '금액' || key === 'price' || key === '수량' || key === 'qty') {
|
||||
const numA = typeof valA === 'number' ? valA : parseInt(String(valA || '0').replace(/[^0-9-]/g, ''), 10);
|
||||
const numB = typeof valB === 'number' ? valB : parseInt(String(valB || '0').replace(/[^0-9-]/g, ''), 10);
|
||||
return direction === 'asc' ? numA - numB : numB - numA;
|
||||
}
|
||||
|
||||
// 문자열 정렬 (기본)
|
||||
valA = String(valA || '').toLowerCase();
|
||||
valB = String(valB || '').toLowerCase();
|
||||
|
||||
if (valA < valB) return direction === 'asc' ? -1 : 1;
|
||||
if (valA > valB) return direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 목록 뷰용 액션 버튼 HTML 생성 (중복 제거를 위해 비워둠)
|
||||
*/
|
||||
export function getActionButtonsHTML(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 100점 만점 감점형 PC 성능 점수 계산 (CPU + RAM + GPU + 연식)
|
||||
*/
|
||||
export function calculatePcScoreDeductive(cpu: string, ram: string, gpu: string, purchaseDate: string): number {
|
||||
let score = 100;
|
||||
if (!cpu) cpu = '';
|
||||
if (!ram) ram = '';
|
||||
if (!gpu) gpu = '';
|
||||
|
||||
const cpuUpper = cpu.toUpperCase();
|
||||
const ramUpper = ram.toUpperCase();
|
||||
const gpuUpper = gpu.toUpperCase();
|
||||
|
||||
// 1. CPU 등급 감점 (최대 -30점)
|
||||
let cpuDeduction = 0;
|
||||
if (cpuUpper.includes('I9') || cpuUpper.includes('RYZEN 9') || cpuUpper.includes('RYZEN9')) {
|
||||
cpuDeduction = 0;
|
||||
} else if (cpuUpper.includes('I7') || cpuUpper.includes('RYZEN 7') || cpuUpper.includes('RYZEN7')) {
|
||||
cpuDeduction = 5;
|
||||
} else if (cpuUpper.includes('I5') || cpuUpper.includes('RYZEN 5') || cpuUpper.includes('RYZEN5')) {
|
||||
cpuDeduction = 15;
|
||||
} else if (cpuUpper.includes('I3') || cpuUpper.includes('RYZEN 3') || cpuUpper.includes('RYZEN3')) {
|
||||
cpuDeduction = 25;
|
||||
} else {
|
||||
cpuDeduction = 30;
|
||||
}
|
||||
score -= cpuDeduction;
|
||||
|
||||
// 2. CPU 세대 노후 감점 (최대 -15점)
|
||||
let genDeduction = 0;
|
||||
const intelMatch = cpuUpper.match(/I\d-?(\d+)/);
|
||||
let gen = 0;
|
||||
if (intelMatch && intelMatch[1]) {
|
||||
const numStr = intelMatch[1];
|
||||
if (numStr.length === 5) gen = parseInt(numStr.substring(0, 2), 10);
|
||||
else if (numStr.length === 4) gen = parseInt(numStr.substring(0, 1), 10);
|
||||
}
|
||||
|
||||
const amdMatch = cpuUpper.match(/RYZEN\s?\d\s?-?(\d+)/);
|
||||
let amdGen = 0;
|
||||
if (amdMatch && amdMatch[1] && !intelMatch) {
|
||||
const numStr = amdMatch[1];
|
||||
if (numStr.length === 4) amdGen = parseInt(numStr.substring(0, 1), 10);
|
||||
}
|
||||
|
||||
if (intelMatch) {
|
||||
if (gen >= 12) genDeduction = 0;
|
||||
else if (gen >= 10) genDeduction = 5;
|
||||
else if (gen >= 8) genDeduction = 10;
|
||||
else genDeduction = 15;
|
||||
} else if (amdMatch) {
|
||||
if (amdGen >= 5) genDeduction = 0;
|
||||
else if (amdGen >= 3) genDeduction = 5;
|
||||
else genDeduction = 10;
|
||||
} else {
|
||||
genDeduction = 15;
|
||||
}
|
||||
score -= genDeduction;
|
||||
|
||||
// 3. RAM 용량 감점 (최대 -25점)
|
||||
const ramMatch = ramUpper.match(/(\d+)\s*GB/);
|
||||
let ramDeduction = 25;
|
||||
if (ramMatch && ramMatch[1]) {
|
||||
const ramVal = parseInt(ramMatch[1], 10);
|
||||
if (ramVal >= 32) ramDeduction = 0;
|
||||
else if (ramVal >= 16) ramDeduction = 10;
|
||||
else if (ramVal >= 8) ramDeduction = 20;
|
||||
else ramDeduction = 25;
|
||||
}
|
||||
score -= ramDeduction;
|
||||
|
||||
// 4. GPU 성능 감점 (최대 -25점)
|
||||
let gpuDeduction = 25;
|
||||
if (!gpuUpper || gpuUpper === '-' || gpuUpper.trim() === '') {
|
||||
gpuDeduction = 25;
|
||||
} else if (
|
||||
gpuUpper.includes('RTX 4090') || gpuUpper.includes('RTX 4080') || gpuUpper.includes('RTX 4070') ||
|
||||
gpuUpper.includes('RTX 3090') || gpuUpper.includes('RTX 3080') ||
|
||||
gpuUpper.includes('RTX A5000') || gpuUpper.includes('RTX A6000') || gpuUpper.includes('RTX A4000')
|
||||
) {
|
||||
gpuDeduction = 0;
|
||||
} else if (
|
||||
gpuUpper.includes('RTX 3070') || gpuUpper.includes('RTX 3060') || gpuUpper.includes('RTX 2060') ||
|
||||
gpuUpper.includes('RTX A2000') || gpuUpper.includes('RTX A3000') || gpuUpper.includes('QUADRO')
|
||||
) {
|
||||
gpuDeduction = 5;
|
||||
} else if (
|
||||
gpuUpper.includes('GTX 1660') || gpuUpper.includes('GTX 1080') || gpuUpper.includes('GTX 1070') ||
|
||||
gpuUpper.includes('GTX 1060') || gpuUpper.includes('RX 6700') || gpuUpper.includes('RX 6600')
|
||||
) {
|
||||
gpuDeduction = 15;
|
||||
} else {
|
||||
gpuDeduction = 25;
|
||||
}
|
||||
score -= gpuDeduction;
|
||||
|
||||
// 5. 연식(노후도) 감점 (최대 -15점)
|
||||
let age = 0;
|
||||
if (purchaseDate && purchaseDate !== '-') {
|
||||
let normalized = purchaseDate.replace(/\./g, '-').trim();
|
||||
if (/^\d{6}$/.test(normalized)) {
|
||||
normalized = `${normalized.substring(0, 4)}-${normalized.substring(4, 6)}`;
|
||||
}
|
||||
const purchase = new Date(normalized);
|
||||
if (!isNaN(purchase.getTime())) {
|
||||
// 2026년 5월 31일 기준 경과연수 계산
|
||||
const mockToday = new Date('2026-05-31');
|
||||
const diffMs = mockToday.getTime() - purchase.getTime();
|
||||
age = diffMs / (1000 * 60 * 60 * 24 * 365.25);
|
||||
age = Math.max(0, parseFloat(age.toFixed(1)));
|
||||
}
|
||||
}
|
||||
|
||||
let ageDeduction = 0;
|
||||
if (age < 1) ageDeduction = 0;
|
||||
else if (age < 2) ageDeduction = 3;
|
||||
else if (age < 3) ageDeduction = 6;
|
||||
else if (age < 4) ageDeduction = 9;
|
||||
else if (age < 5) ageDeduction = 12;
|
||||
else ageDeduction = 15;
|
||||
|
||||
score -= ageDeduction;
|
||||
|
||||
return Math.max(10, score);
|
||||
}
|
||||
|
||||
/**
|
||||
* 성능 점수 기준 등급 뱃지 메타 정보 가져오기
|
||||
*/
|
||||
export function getPcGrade(score: number, isWin11Incompatible?: boolean): { name: string; class: string; color: string } {
|
||||
if (score >= 85) return { name: '최상급', class: 'b-purple', color: '#7C3AED' };
|
||||
if (score >= 70) return { name: '상급', class: 'b-primary', color: '#4F46E5' };
|
||||
if (score >= 40) return { name: '중급', class: 'b-green', color: '#10B981' };
|
||||
if (score >= 20 && !isWin11Incompatible) return { name: '보급', class: 'b-yellow', color: '#F59E0B' };
|
||||
return { name: '교체 대상', class: 'badge-danger', color: '#EF4444' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows 11 업그레이드 지원 불가능한 하드웨어 조건인지 판별
|
||||
*/
|
||||
export function isWindows11Incompatible(cpu: string, ram: string): boolean {
|
||||
if (!cpu) return true;
|
||||
const cpuUpper = cpu.toUpperCase();
|
||||
|
||||
// 1. RAM 4GB 미만은 공식 미지원
|
||||
if (ram) {
|
||||
const ramMatch = ram.toUpperCase().match(/(\d+)\s*GB/);
|
||||
if (ramMatch && ramMatch[1]) {
|
||||
const ramVal = parseInt(ramMatch[1], 10);
|
||||
if (ramVal < 4) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. CPU 세대 검사
|
||||
// Intel CPU 세대 판정
|
||||
const intelMatch = cpuUpper.match(/I\d-?(\d+)/);
|
||||
if (intelMatch && intelMatch[1]) {
|
||||
const numStr = intelMatch[1];
|
||||
let gen = 0;
|
||||
if (numStr.length === 5) gen = parseInt(numStr.substring(0, 2), 10);
|
||||
else if (numStr.length === 4) gen = parseInt(numStr.substring(0, 1), 10);
|
||||
else if (numStr.length === 3) gen = parseInt(numStr.substring(0, 1), 10); // 3자리수 구형 세대 (예: i5-750)
|
||||
|
||||
if (gen > 0 && gen < 8) return true; // 8세대 미만 불가
|
||||
return false;
|
||||
}
|
||||
|
||||
// AMD Ryzen CPU 세대 판정
|
||||
const amdMatch = cpuUpper.match(/RYZEN\s?\d\s?-?(\d+)/);
|
||||
if (amdMatch && amdMatch[1]) {
|
||||
const numStr = amdMatch[1];
|
||||
let amdGen = 0;
|
||||
if (numStr.length === 4) amdGen = parseInt(numStr.substring(0, 1), 10); // 1xxx, 2xxx 등
|
||||
|
||||
if (amdGen > 0 && amdGen < 2) return true; // Ryzen 1세대 이하는 불가
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apple Silicon은 지원
|
||||
if (cpuUpper.includes('APPLE') || cpuUpper.includes('M1') || cpuUpper.includes('M2') || cpuUpper.includes('M3') || cpuUpper.includes('M4')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 그 외 확실한 구형 CPU 제품군
|
||||
const knownOldCpus = ['CORE2', 'CORE 2', 'PENTIUM', 'CELERON', 'ATHLON', 'PHENOM', 'XEON'];
|
||||
if (knownOldCpus.some(name => cpuUpper.includes(name))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 세대 매칭은 안되었으나 Intel Core i 시리즈 구조이면 구형(1세대 등)으로 간주
|
||||
if (cpuUpper.includes('I3') || cpuUpper.includes('I5') || cpuUpper.includes('I7') || cpuUpper.includes('I9')) {
|
||||
// i5-620M 처럼 옛날 구형 모바일 칩 등
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
import { PAGE_DESCRIPTIONS } from './schema';
|
||||
|
||||
export const API_BASE_URL = '';
|
||||
|
||||
/**
|
||||
* ITAM 공통 유틸리티 함수
|
||||
*/
|
||||
|
||||
/**
|
||||
* 페이지 헤더(타이틀 및 설명) 렌더링
|
||||
*/
|
||||
export function renderPageHeader(container: HTMLElement, pageId: string) {
|
||||
const config = PAGE_DESCRIPTIONS[pageId];
|
||||
if (!config) return;
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'page-header';
|
||||
header.innerHTML = `
|
||||
<div class="page-title-group">
|
||||
<h2 class="page-title">${config.title}</h2>
|
||||
<p class="page-description">${config.description}</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 숫자에 천 단위 콤마 추가 (금액 표시용)
|
||||
*/
|
||||
export function formatPrice(value: string | number): string {
|
||||
if (value === undefined || value === null) return '';
|
||||
const num = String(value).replace(/[^0-9]/g, '');
|
||||
if (!num) return '';
|
||||
return num.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 배지 생성 (정/부 담당자, 원격도구 등)
|
||||
*/
|
||||
export function createBadge(text: string, type: 'primary' | 'muted' | 'success' | 'danger' = 'primary'): string {
|
||||
return `<span class="badge badge-${type}">${text}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트 내 줄바꿈을 구분자(/)로 변경하여 한 줄로 표시
|
||||
*/
|
||||
export function formatInline(value: any): string {
|
||||
return String(value || '').replace(/\n/g, ' / ').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 문자열 포맷팅 (YYYY.MM.DD -> YYYY-MM-DD)
|
||||
*/
|
||||
export function normalizeDate(dateStr: string): string {
|
||||
if (!dateStr) return '';
|
||||
let str = String(dateStr).replace(/\./g, '-').trim();
|
||||
// YYYYMM 형식 처리 (6자리 숫자)
|
||||
if (/^\d{6}$/.test(str)) {
|
||||
return `${str.substring(0, 4)}-${str.substring(4, 6)}`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 구매일로부터 현재까지의 경과 연수 계산 (소수점 첫째자리)
|
||||
*/
|
||||
export function calculateAssetAge(purchaseDate: string): number {
|
||||
const normalized = normalizeDate(purchaseDate);
|
||||
if (!normalized) return 0;
|
||||
|
||||
const purchase = new Date(normalized);
|
||||
if (isNaN(purchase.getTime())) return 0;
|
||||
|
||||
const diffMs = Date.now() - purchase.getTime();
|
||||
const age = diffMs / (1000 * 60 * 60 * 24 * 365.25);
|
||||
return Math.max(0, parseFloat(age.toFixed(1)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 고유 ID 생성 (7자리 랜덤 문자열)
|
||||
*/
|
||||
export function generateId(): string {
|
||||
return Math.random().toString(36).substring(2, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 자산 객체 간의 변경 사항 감지
|
||||
*/
|
||||
export function getAssetChanges(oldAsset: any, newAsset: any, fields: {key: string, label: string}[]): string {
|
||||
const changes: string[] = [];
|
||||
fields.forEach(field => {
|
||||
const oldVal = String(oldAsset[field.key] || '').trim();
|
||||
const newVal = String(newAsset[field.key] || '').trim();
|
||||
if (oldVal !== newVal) {
|
||||
changes.push(`${field.label}: ${oldVal || '없음'} → ${newVal || '없음'}`);
|
||||
}
|
||||
});
|
||||
return changes.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 자산 목록 정렬 (기본: 법인별 -> 자산번호 순)
|
||||
*/
|
||||
export function sortAssets<T>(list: T[]): T[] {
|
||||
return [...list].sort((a: any, b: any) => {
|
||||
// 1순위: 법인 (가나다순)
|
||||
const corpA = String(a.법인 || a.corp || '').trim();
|
||||
const corpB = String(b.법인 || b.corp || '').trim();
|
||||
if (corpA < corpB) return -1;
|
||||
if (corpA > corpB) return 1;
|
||||
|
||||
// 2순위: 자산번호/코드 (영문/숫자순)
|
||||
const codeA = String(a.자산코드 || a.자산번호 || a.id || '').trim();
|
||||
const codeB = String(b.자산코드 || b.자산번호 || b.id || '').trim();
|
||||
if (codeA < codeB) return -1;
|
||||
if (codeA > codeB) return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 동적 정렬 함수
|
||||
* @param list 정렬할 목록
|
||||
* @param key 정렬 기준 필드
|
||||
* @param direction 정렬 방향 ('asc' | 'desc')
|
||||
*/
|
||||
export function dynamicSort<T>(list: T[], key: string, direction: 'asc' | 'desc'): T[] {
|
||||
return [...list].sort((a: any, b: any) => {
|
||||
let valA = a[key];
|
||||
let valB = b[key];
|
||||
|
||||
// 숫자인 경우 처리
|
||||
if (typeof valA === 'number' && typeof valB === 'number') {
|
||||
return direction === 'asc' ? valA - valB : valB - valA;
|
||||
}
|
||||
|
||||
// 금액 필드 (숫자형 문자열 포함) 처리
|
||||
if (key === '금액' || key === 'price' || key === '수량' || key === 'qty') {
|
||||
const numA = typeof valA === 'number' ? valA : parseInt(String(valA || '0').replace(/[^0-9-]/g, ''), 10);
|
||||
const numB = typeof valB === 'number' ? valB : parseInt(String(valB || '0').replace(/[^0-9-]/g, ''), 10);
|
||||
return direction === 'asc' ? numA - numB : numB - numA;
|
||||
}
|
||||
|
||||
// 문자열 정렬 (기본)
|
||||
valA = String(valA || '').toLowerCase();
|
||||
valB = String(valB || '').toLowerCase();
|
||||
|
||||
if (valA < valB) return direction === 'asc' ? -1 : 1;
|
||||
if (valA > valB) return direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 목록 뷰용 액션 버튼 HTML 생성 (중복 제거를 위해 비워둠)
|
||||
*/
|
||||
export function getActionButtonsHTML(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 100점 만점 감점형 PC 성능 점수 계산 (CPU + RAM + GPU + 연식)
|
||||
*/
|
||||
export function calculatePcScoreDeductive(cpu: string, ram: string, gpu: string, purchaseDate: string): number {
|
||||
let score = 100;
|
||||
if (!cpu) cpu = '';
|
||||
if (!ram) ram = '';
|
||||
if (!gpu) gpu = '';
|
||||
|
||||
const cpuUpper = cpu.toUpperCase();
|
||||
const ramUpper = ram.toUpperCase();
|
||||
const gpuUpper = gpu.toUpperCase();
|
||||
|
||||
// 1. CPU 등급 감점 (최대 -30점)
|
||||
let cpuDeduction = 0;
|
||||
if (cpuUpper.includes('I9') || cpuUpper.includes('RYZEN 9') || cpuUpper.includes('RYZEN9')) {
|
||||
cpuDeduction = 0;
|
||||
} else if (cpuUpper.includes('I7') || cpuUpper.includes('RYZEN 7') || cpuUpper.includes('RYZEN7')) {
|
||||
cpuDeduction = 5;
|
||||
} else if (cpuUpper.includes('I5') || cpuUpper.includes('RYZEN 5') || cpuUpper.includes('RYZEN5')) {
|
||||
cpuDeduction = 15;
|
||||
} else if (cpuUpper.includes('I3') || cpuUpper.includes('RYZEN 3') || cpuUpper.includes('RYZEN3')) {
|
||||
cpuDeduction = 25;
|
||||
} else {
|
||||
cpuDeduction = 30;
|
||||
}
|
||||
score -= cpuDeduction;
|
||||
|
||||
// 2. CPU 세대 노후 감점 (최대 -15점)
|
||||
let genDeduction = 0;
|
||||
const intelMatch = cpuUpper.match(/I\d-?(\d+)/);
|
||||
let gen = 0;
|
||||
if (intelMatch && intelMatch[1]) {
|
||||
const numStr = intelMatch[1];
|
||||
if (numStr.length === 5) gen = parseInt(numStr.substring(0, 2), 10);
|
||||
else if (numStr.length === 4) gen = parseInt(numStr.substring(0, 1), 10);
|
||||
}
|
||||
|
||||
const amdMatch = cpuUpper.match(/RYZEN\s?\d\s?-?(\d+)/);
|
||||
let amdGen = 0;
|
||||
if (amdMatch && amdMatch[1] && !intelMatch) {
|
||||
const numStr = amdMatch[1];
|
||||
if (numStr.length === 4) amdGen = parseInt(numStr.substring(0, 1), 10);
|
||||
}
|
||||
|
||||
if (intelMatch) {
|
||||
if (gen >= 12) genDeduction = 0;
|
||||
else if (gen >= 10) genDeduction = 5;
|
||||
else if (gen >= 8) genDeduction = 10;
|
||||
else genDeduction = 15;
|
||||
} else if (amdMatch) {
|
||||
if (amdGen >= 5) genDeduction = 0;
|
||||
else if (amdGen >= 3) genDeduction = 5;
|
||||
else genDeduction = 10;
|
||||
} else {
|
||||
genDeduction = 15;
|
||||
}
|
||||
score -= genDeduction;
|
||||
|
||||
// 3. RAM 용량 감점 (최대 -25점)
|
||||
const ramMatch = ramUpper.match(/(\d+)\s*GB/);
|
||||
let ramDeduction = 25;
|
||||
if (ramMatch && ramMatch[1]) {
|
||||
const ramVal = parseInt(ramMatch[1], 10);
|
||||
if (ramVal >= 32) ramDeduction = 0;
|
||||
else if (ramVal >= 16) ramDeduction = 10;
|
||||
else if (ramVal >= 8) ramDeduction = 20;
|
||||
else ramDeduction = 25;
|
||||
}
|
||||
score -= ramDeduction;
|
||||
|
||||
// 4. GPU 성능 감점 (최대 -25점)
|
||||
let gpuDeduction = 25;
|
||||
if (!gpuUpper || gpuUpper === '-' || gpuUpper.trim() === '') {
|
||||
gpuDeduction = 25;
|
||||
} else if (
|
||||
gpuUpper.includes('RTX 4090') || gpuUpper.includes('RTX 4080') || gpuUpper.includes('RTX 4070') ||
|
||||
gpuUpper.includes('RTX 3090') || gpuUpper.includes('RTX 3080') ||
|
||||
gpuUpper.includes('RTX A5000') || gpuUpper.includes('RTX A6000') || gpuUpper.includes('RTX A4000')
|
||||
) {
|
||||
gpuDeduction = 0;
|
||||
} else if (
|
||||
gpuUpper.includes('RTX 3070') || gpuUpper.includes('RTX 3060') || gpuUpper.includes('RTX 2060') ||
|
||||
gpuUpper.includes('RTX A2000') || gpuUpper.includes('RTX A3000') || gpuUpper.includes('QUADRO')
|
||||
) {
|
||||
gpuDeduction = 5;
|
||||
} else if (
|
||||
gpuUpper.includes('GTX 1660') || gpuUpper.includes('GTX 1080') || gpuUpper.includes('GTX 1070') ||
|
||||
gpuUpper.includes('GTX 1060') || gpuUpper.includes('RX 6700') || gpuUpper.includes('RX 6600')
|
||||
) {
|
||||
gpuDeduction = 15;
|
||||
} else {
|
||||
gpuDeduction = 25;
|
||||
}
|
||||
score -= gpuDeduction;
|
||||
|
||||
// 5. 연식(노후도) 감점 (최대 -15점)
|
||||
let age = 0;
|
||||
if (purchaseDate && purchaseDate !== '-') {
|
||||
let normalized = purchaseDate.replace(/\./g, '-').trim();
|
||||
if (/^\d{6}$/.test(normalized)) {
|
||||
normalized = `${normalized.substring(0, 4)}-${normalized.substring(4, 6)}`;
|
||||
}
|
||||
const purchase = new Date(normalized);
|
||||
if (!isNaN(purchase.getTime())) {
|
||||
// 2026년 5월 31일 기준 경과연수 계산
|
||||
const mockToday = new Date('2026-05-31');
|
||||
const diffMs = mockToday.getTime() - purchase.getTime();
|
||||
age = diffMs / (1000 * 60 * 60 * 24 * 365.25);
|
||||
age = Math.max(0, parseFloat(age.toFixed(1)));
|
||||
}
|
||||
}
|
||||
|
||||
let ageDeduction = 0;
|
||||
if (age < 1) ageDeduction = 0;
|
||||
else if (age < 2) ageDeduction = 3;
|
||||
else if (age < 3) ageDeduction = 6;
|
||||
else if (age < 4) ageDeduction = 9;
|
||||
else if (age < 5) ageDeduction = 12;
|
||||
else ageDeduction = 15;
|
||||
|
||||
score -= ageDeduction;
|
||||
|
||||
return Math.max(10, score);
|
||||
}
|
||||
|
||||
/**
|
||||
* 성능 점수 기준 등급 뱃지 메타 정보 가져오기
|
||||
*/
|
||||
export function getPcGrade(score: number, isWin11Incompatible?: boolean): { name: string; class: string; color: string } {
|
||||
if (score >= 85) return { name: '최상급', class: 'b-purple', color: '#7C3AED' };
|
||||
if (score >= 70) return { name: '상급', class: 'b-primary', color: '#4F46E5' };
|
||||
if (score >= 40) return { name: '중급', class: 'b-green', color: '#10B981' };
|
||||
if (score >= 20 && !isWin11Incompatible) return { name: '보급', class: 'b-yellow', color: '#F59E0B' };
|
||||
return { name: '교체 대상', class: 'badge-danger', color: '#EF4444' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows 11 업그레이드 지원 불가능한 하드웨어 조건인지 판별
|
||||
*/
|
||||
export function isWindows11Incompatible(cpu: string, ram: string): boolean {
|
||||
if (!cpu) return true;
|
||||
const cpuUpper = cpu.toUpperCase();
|
||||
|
||||
// 1. RAM 4GB 미만은 공식 미지원
|
||||
if (ram) {
|
||||
const ramMatch = ram.toUpperCase().match(/(\d+)\s*GB/);
|
||||
if (ramMatch && ramMatch[1]) {
|
||||
const ramVal = parseInt(ramMatch[1], 10);
|
||||
if (ramVal < 4) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. CPU 세대 검사
|
||||
// Intel CPU 세대 판정
|
||||
const intelMatch = cpuUpper.match(/I\d-?(\d+)/);
|
||||
if (intelMatch && intelMatch[1]) {
|
||||
const numStr = intelMatch[1];
|
||||
let gen = 0;
|
||||
if (numStr.length === 5) gen = parseInt(numStr.substring(0, 2), 10);
|
||||
else if (numStr.length === 4) gen = parseInt(numStr.substring(0, 1), 10);
|
||||
else if (numStr.length === 3) gen = parseInt(numStr.substring(0, 1), 10); // 3자리수 구형 세대 (예: i5-750)
|
||||
|
||||
if (gen > 0 && gen < 8) return true; // 8세대 미만 불가
|
||||
return false;
|
||||
}
|
||||
|
||||
// AMD Ryzen CPU 세대 판정
|
||||
const amdMatch = cpuUpper.match(/RYZEN\s?\d\s?-?(\d+)/);
|
||||
if (amdMatch && amdMatch[1]) {
|
||||
const numStr = amdMatch[1];
|
||||
let amdGen = 0;
|
||||
if (numStr.length === 4) amdGen = parseInt(numStr.substring(0, 1), 10); // 1xxx, 2xxx 등
|
||||
|
||||
if (amdGen > 0 && amdGen < 2) return true; // Ryzen 1세대 이하는 불가
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apple Silicon은 지원
|
||||
if (cpuUpper.includes('APPLE') || cpuUpper.includes('M1') || cpuUpper.includes('M2') || cpuUpper.includes('M3') || cpuUpper.includes('M4')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 그 외 확실한 구형 CPU 제품군
|
||||
const knownOldCpus = ['CORE2', 'CORE 2', 'PENTIUM', 'CELERON', 'ATHLON', 'PHENOM', 'XEON'];
|
||||
if (knownOldCpus.some(name => cpuUpper.includes(name))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 세대 매칭은 안되었으나 Intel Core i 시리즈 구조이면 구형(1세대 등)으로 간주
|
||||
if (cpuUpper.includes('I3') || cpuUpper.includes('I5') || cpuUpper.includes('I7') || cpuUpper.includes('I9')) {
|
||||
// i5-620M 처럼 옛날 구형 모바일 칩 등
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
410
src/main.ts
410
src/main.ts
@@ -1,205 +1,205 @@
|
||||
import './styles/common.css';
|
||||
import './styles/login.css';
|
||||
import { state, loadMasterDataFromDB, saveAsset } from './core/state';
|
||||
import { renderNavigation } from './components/Navigation';
|
||||
import { renderDashboard } from './views/DashboardView';
|
||||
import { renderSWTable } from './views/SW_Table';
|
||||
import { renderLocationView } from './views/LocationView';
|
||||
import { initBaseModal } from './components/Modal/BaseModal';
|
||||
import { initHwModal, openHwModal } from './components/Modal/HWModal';
|
||||
import { initSwModal, openSwModal } from './components/Modal/SWModal';
|
||||
import { initSwUserModal } from './components/Modal/SWUserModal';
|
||||
import { initDomainModal, openDomainModal } from './components/Modal/DomainModal';
|
||||
import { initPartsMasterModal, openPartsMasterModal } from './components/Modal/PartsMasterModal';
|
||||
import { initJobSpecModal, openJobSpecModal } from './components/Modal/JobSpecModal';
|
||||
import { initUserModal, openUserModal } from './components/Modal/UserModal';
|
||||
import { activePartsMasterSubTab } from './views/List/PartsMasterListView';
|
||||
import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal';
|
||||
import { initGuide } from './components/Guide';
|
||||
import { pcFlowModal } from './components/Modal/PCFlowModal';
|
||||
import { createIcons, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings } from 'lucide';
|
||||
|
||||
|
||||
// 화면 갱신 통합 핸들러
|
||||
function refreshView(tab?: string) {
|
||||
const mainContent = document.getElementById('main-content')!;
|
||||
if (!mainContent) return;
|
||||
|
||||
const activeTab = tab || state.activeSubTab;
|
||||
|
||||
if (activeTab === '대시보드') {
|
||||
renderDashboard(mainContent);
|
||||
return;
|
||||
}
|
||||
|
||||
// 서버 탭이 아닐 경우에는 state.viewMode가 location이더라도 강제로 목록(list) 뷰를 그리도록 함
|
||||
// (state.viewMode의 원래 상태는 보존하여, 서버 탭 복귀 시 최근 보던 모드를 유지함)
|
||||
const isServerTab = activeTab === '서버';
|
||||
const effectiveViewMode = isServerTab ? state.viewMode : 'list';
|
||||
|
||||
mainContent.innerHTML = `
|
||||
<div id="view-body" class="view-container"></div>
|
||||
`;
|
||||
|
||||
const viewBody = document.getElementById('view-body')!;
|
||||
if (effectiveViewMode === 'location') {
|
||||
renderLocationView(viewBody);
|
||||
} else {
|
||||
renderSWTable(viewBody); // 리스트 형식
|
||||
}
|
||||
}
|
||||
|
||||
// 통합 갱신 (저장은 이미 개별 모달에서 처리됨)
|
||||
async function refreshAllData() {
|
||||
await loadMasterDataFromDB();
|
||||
refreshView();
|
||||
}
|
||||
|
||||
// --- App Initialization ---
|
||||
function initApp() {
|
||||
const mainContent = document.getElementById('main-content')!;
|
||||
if (!mainContent) return;
|
||||
|
||||
const { closeAllModals } = initBaseModal();
|
||||
|
||||
try {
|
||||
renderNavigation((tab) => {
|
||||
refreshView();
|
||||
});
|
||||
|
||||
initHwModal(() => refreshAllData(), closeAllModals);
|
||||
initSwModal(() => refreshAllData(), closeAllModals);
|
||||
initSwUserModal(() => {
|
||||
loadMasterDataFromDB().then(() => refreshView());
|
||||
}, closeAllModals);
|
||||
initDomainModal(() => refreshAllData(), closeAllModals);
|
||||
initPartsMasterModal(() => refreshAllData(), closeAllModals);
|
||||
initJobSpecModal(() => refreshAllData(), closeAllModals);
|
||||
initUserModal(() => refreshAllData(), closeAllModals);
|
||||
|
||||
initDashboardDetailModal();
|
||||
initGuide();
|
||||
pcFlowModal.init(() => {
|
||||
loadMasterDataFromDB().then(() => refreshView());
|
||||
});
|
||||
|
||||
loadMasterDataFromDB().then((success) => {
|
||||
if (success) {
|
||||
refreshView();
|
||||
initRoleSwitcher(); // [추가] 역할 전환 토글 초기화
|
||||
}
|
||||
});
|
||||
} catch (e) { console.error('❌ Initialization failed:', e); }
|
||||
|
||||
console.log('🚀 ITAM App Multi-Table Optimized');
|
||||
|
||||
// --- 통합 이벤트 위임 (Dynamic Elements 지원) ---
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// 자산 추가
|
||||
if (target.closest('#btn-add-asset')) {
|
||||
const tab = state.activeSubTab;
|
||||
const cat = state.activeCategory;
|
||||
const newId = Math.random().toString(36).substring(2, 9);
|
||||
|
||||
if (cat === 'hw') {
|
||||
if (tab === '부품 마스터') {
|
||||
if (activePartsMasterSubTab === 'job-spec') {
|
||||
openJobSpecModal({ id: '' } as any, 'add');
|
||||
} else {
|
||||
openPartsMasterModal({ id: '' } as any, 'add');
|
||||
}
|
||||
} else {
|
||||
openHwModal({ id: newId, asset_code: '', category: tab } as any, 'add');
|
||||
}
|
||||
} else if (cat === 'sw') {
|
||||
const swType = tab === '외부SW' ? '외부SW' : (tab === '내부SW' ? '내부SW' : '외부SW');
|
||||
openSwModal({ id: newId, asset_type: swType } as any, 'add');
|
||||
} else if (cat === 'ops') {
|
||||
if (tab === '도메인') openDomainModal(null);
|
||||
else if (tab === '사용자') openUserModal({ id: '' }, 'add');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 부품 마스터 탭으로 바로가기 연동
|
||||
if (target.closest('#btn-goto-parts-master')) {
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '부품 마스터';
|
||||
renderNavigation((tab) => { refreshView(); });
|
||||
refreshView();
|
||||
return;
|
||||
}
|
||||
|
||||
// PC 이동/반납 모달 열기
|
||||
if (target.closest('#btn-pc-flow')) {
|
||||
pcFlowModal.open();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
createIcons({
|
||||
icons: { Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings }
|
||||
});
|
||||
window.addEventListener('refresh-view', () => refreshView());
|
||||
}
|
||||
|
||||
/**
|
||||
* 헤더 역할 전환 토글 로직
|
||||
*/
|
||||
function initRoleSwitcher() {
|
||||
const checkbox = document.getElementById('role-toggle-checkbox') as HTMLInputElement;
|
||||
const userLabel = document.querySelector('.role-label.user');
|
||||
const adminLabel = document.querySelector('.role-label.admin');
|
||||
|
||||
if (!checkbox || !userLabel || !adminLabel) return;
|
||||
|
||||
checkbox.addEventListener('change', () => {
|
||||
if (checkbox.checked) {
|
||||
state.currentUserRole = 'admin';
|
||||
userLabel.classList.remove('active');
|
||||
adminLabel.classList.add('active');
|
||||
document.body.classList.add('admin-mode');
|
||||
|
||||
// 관리자 모드 전환 시 대시보드로 이동
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '대시보드';
|
||||
} else {
|
||||
state.currentUserRole = 'user';
|
||||
adminLabel.classList.remove('active');
|
||||
userLabel.classList.add('active');
|
||||
document.body.classList.remove('admin-mode');
|
||||
|
||||
// 실무자 모드 전환 시 서버 목록으로 이동
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '서버';
|
||||
}
|
||||
// 모든 렌더링을 refreshView 하나로 통합하여 규격 유지
|
||||
renderNavigation(() => refreshView());
|
||||
refreshView();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 앱 초기화 (로그인 과정 없이 즉시 시작)
|
||||
*/
|
||||
function initializeAppDirectly() {
|
||||
const loginContainer = document.getElementById('login-container');
|
||||
const appLayout = document.getElementById('app-layout');
|
||||
|
||||
// 기본 권한 설정: 실무자 (User)
|
||||
state.currentUserRole = 'user';
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '서버'; // 실무자 기본 탭
|
||||
|
||||
// 화면 전환
|
||||
if (loginContainer) loginContainer.style.display = 'none';
|
||||
if (appLayout) appLayout.style.display = 'flex';
|
||||
|
||||
// 앱 초기화 및 내비게이션(헤더 포함) 렌더링
|
||||
initApp();
|
||||
renderNavigation((tab) => refreshView(tab));
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initializeAppDirectly);
|
||||
import './styles/common.css';
|
||||
import './styles/login.css';
|
||||
import { state, loadMasterDataFromDB, saveAsset } from './core/state';
|
||||
import { renderNavigation } from './components/Navigation';
|
||||
import { renderDashboard } from './views/DashboardView';
|
||||
import { renderSWTable } from './views/SW_Table';
|
||||
import { renderLocationView } from './views/LocationView';
|
||||
import { initBaseModal } from './components/Modal/BaseModal';
|
||||
import { initHwModal, openHwModal } from './components/Modal/HWModal';
|
||||
import { initSwModal, openSwModal } from './components/Modal/SWModal';
|
||||
import { initSwUserModal } from './components/Modal/SWUserModal';
|
||||
import { initDomainModal, openDomainModal } from './components/Modal/DomainModal';
|
||||
import { initPartsMasterModal, openPartsMasterModal } from './components/Modal/PartsMasterModal';
|
||||
import { initJobSpecModal, openJobSpecModal } from './components/Modal/JobSpecModal';
|
||||
import { initUserModal, openUserModal } from './components/Modal/UserModal';
|
||||
import { activePartsMasterSubTab } from './views/List/PartsMasterListView';
|
||||
import { initDashboardDetailModal } from './components/Modal/DashboardDetailModal';
|
||||
import { initGuide } from './components/Guide';
|
||||
import { pcFlowModal } from './components/Modal/PCFlowModal';
|
||||
import { createIcons, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings } from 'lucide';
|
||||
|
||||
|
||||
// 화면 갱신 통합 핸들러
|
||||
function refreshView(tab?: string) {
|
||||
const mainContent = document.getElementById('main-content')!;
|
||||
if (!mainContent) return;
|
||||
|
||||
const activeTab = tab || state.activeSubTab;
|
||||
|
||||
if (activeTab === '대시보드') {
|
||||
renderDashboard(mainContent);
|
||||
return;
|
||||
}
|
||||
|
||||
// 서버 탭이 아닐 경우에는 state.viewMode가 location이더라도 강제로 목록(list) 뷰를 그리도록 함
|
||||
// (state.viewMode의 원래 상태는 보존하여, 서버 탭 복귀 시 최근 보던 모드를 유지함)
|
||||
const isServerTab = activeTab === '서버';
|
||||
const effectiveViewMode = isServerTab ? state.viewMode : 'list';
|
||||
|
||||
mainContent.innerHTML = `
|
||||
<div id="view-body" class="view-container"></div>
|
||||
`;
|
||||
|
||||
const viewBody = document.getElementById('view-body')!;
|
||||
if (effectiveViewMode === 'location') {
|
||||
renderLocationView(viewBody);
|
||||
} else {
|
||||
renderSWTable(viewBody); // 리스트 형식
|
||||
}
|
||||
}
|
||||
|
||||
// 통합 갱신 (저장은 이미 개별 모달에서 처리됨)
|
||||
async function refreshAllData() {
|
||||
await loadMasterDataFromDB();
|
||||
refreshView();
|
||||
}
|
||||
|
||||
// --- App Initialization ---
|
||||
function initApp() {
|
||||
const mainContent = document.getElementById('main-content')!;
|
||||
if (!mainContent) return;
|
||||
|
||||
const { closeAllModals } = initBaseModal();
|
||||
|
||||
try {
|
||||
renderNavigation((tab) => {
|
||||
refreshView();
|
||||
});
|
||||
|
||||
initHwModal(() => refreshAllData(), closeAllModals);
|
||||
initSwModal(() => refreshAllData(), closeAllModals);
|
||||
initSwUserModal(() => {
|
||||
loadMasterDataFromDB().then(() => refreshView());
|
||||
}, closeAllModals);
|
||||
initDomainModal(() => refreshAllData(), closeAllModals);
|
||||
initPartsMasterModal(() => refreshAllData(), closeAllModals);
|
||||
initJobSpecModal(() => refreshAllData(), closeAllModals);
|
||||
initUserModal(() => refreshAllData(), closeAllModals);
|
||||
|
||||
initDashboardDetailModal();
|
||||
initGuide();
|
||||
pcFlowModal.init(() => {
|
||||
loadMasterDataFromDB().then(() => refreshView());
|
||||
});
|
||||
|
||||
loadMasterDataFromDB().then((success) => {
|
||||
if (success) {
|
||||
refreshView();
|
||||
initRoleSwitcher(); // [추가] 역할 전환 토글 초기화
|
||||
}
|
||||
});
|
||||
} catch (e) { console.error('❌ Initialization failed:', e); }
|
||||
|
||||
console.log('🚀 ITAM App Multi-Table Optimized');
|
||||
|
||||
// --- 통합 이벤트 위임 (Dynamic Elements 지원) ---
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// 자산 추가
|
||||
if (target.closest('#btn-add-asset')) {
|
||||
const tab = state.activeSubTab;
|
||||
const cat = state.activeCategory;
|
||||
const newId = Math.random().toString(36).substring(2, 9);
|
||||
|
||||
if (cat === 'hw') {
|
||||
if (tab === '부품 마스터') {
|
||||
if (activePartsMasterSubTab === 'job-spec') {
|
||||
openJobSpecModal({ id: '' } as any, 'add');
|
||||
} else {
|
||||
openPartsMasterModal({ id: '' } as any, 'add');
|
||||
}
|
||||
} else {
|
||||
openHwModal({ id: newId, asset_code: '', category: tab } as any, 'add');
|
||||
}
|
||||
} else if (cat === 'sw') {
|
||||
const swType = tab === '외부SW' ? '외부SW' : (tab === '내부SW' ? '내부SW' : '외부SW');
|
||||
openSwModal({ id: newId, asset_type: swType } as any, 'add');
|
||||
} else if (cat === 'ops') {
|
||||
if (tab === '도메인') openDomainModal(null);
|
||||
else if (tab === '사용자') openUserModal({ id: '' }, 'add');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 부품 마스터 탭으로 바로가기 연동
|
||||
if (target.closest('#btn-goto-parts-master')) {
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '부품 마스터';
|
||||
renderNavigation((tab) => { refreshView(); });
|
||||
refreshView();
|
||||
return;
|
||||
}
|
||||
|
||||
// PC 이동/반납 모달 열기
|
||||
if (target.closest('#btn-pc-flow')) {
|
||||
pcFlowModal.open();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
createIcons({
|
||||
icons: { Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings }
|
||||
});
|
||||
window.addEventListener('refresh-view', () => refreshView());
|
||||
}
|
||||
|
||||
/**
|
||||
* 헤더 역할 전환 토글 로직
|
||||
*/
|
||||
function initRoleSwitcher() {
|
||||
const checkbox = document.getElementById('role-toggle-checkbox') as HTMLInputElement;
|
||||
const userLabel = document.querySelector('.role-label.user');
|
||||
const adminLabel = document.querySelector('.role-label.admin');
|
||||
|
||||
if (!checkbox || !userLabel || !adminLabel) return;
|
||||
|
||||
checkbox.addEventListener('change', () => {
|
||||
if (checkbox.checked) {
|
||||
state.currentUserRole = 'admin';
|
||||
userLabel.classList.remove('active');
|
||||
adminLabel.classList.add('active');
|
||||
document.body.classList.add('admin-mode');
|
||||
|
||||
// 관리자 모드 전환 시 대시보드로 이동
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '대시보드';
|
||||
} else {
|
||||
state.currentUserRole = 'user';
|
||||
adminLabel.classList.remove('active');
|
||||
userLabel.classList.add('active');
|
||||
document.body.classList.remove('admin-mode');
|
||||
|
||||
// 실무자 모드 전환 시 서버 목록으로 이동
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '서버';
|
||||
}
|
||||
// 모든 렌더링을 refreshView 하나로 통합하여 규격 유지
|
||||
renderNavigation(() => refreshView());
|
||||
refreshView();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 앱 초기화 (로그인 과정 없이 즉시 시작)
|
||||
*/
|
||||
function initializeAppDirectly() {
|
||||
const loginContainer = document.getElementById('login-container');
|
||||
const appLayout = document.getElementById('app-layout');
|
||||
|
||||
// 기본 권한 설정: 실무자 (User)
|
||||
state.currentUserRole = 'user';
|
||||
state.activeCategory = 'hw';
|
||||
state.activeSubTab = '서버'; // 실무자 기본 탭
|
||||
|
||||
// 화면 전환
|
||||
if (loginContainer) loginContainer.style.display = 'none';
|
||||
if (appLayout) appLayout.style.display = 'flex';
|
||||
|
||||
// 앱 초기화 및 내비게이션(헤더 포함) 렌더링
|
||||
initApp();
|
||||
renderNavigation((tab) => refreshView(tab));
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initializeAppDirectly);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import './styles/common.css';
|
||||
import './views/map-editor.css';
|
||||
import { MapEditor } from './views/MapEditor';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const editor = new MapEditor();
|
||||
editor.init();
|
||||
});
|
||||
import './styles/common.css';
|
||||
import './views/map-editor.css';
|
||||
import { MapEditor } from './views/MapEditor';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const editor = new MapEditor();
|
||||
editor.init();
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,115 +1,115 @@
|
||||
/* Login Screen Styles */
|
||||
|
||||
.login-layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background-color: var(--bg-color);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
background-color: var(--white);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 3rem;
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.08);
|
||||
animation: slideUp 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
height: 52px;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
font-size: 2.33rem;
|
||||
font-weight: 900;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.login-selection {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.role-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 2rem 1.5rem;
|
||||
border: 2px solid var(--bg-light);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
|
||||
.role-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
background-color: var(--white);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 20px rgba(30, 81, 73, 0.08);
|
||||
}
|
||||
|
||||
.role-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background-color: var(--white);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.25rem;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.role-card:hover .role-icon {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--white);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.role-card h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.role-card p {
|
||||
font-size: 1.08rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: 3rem;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Login Screen Styles */
|
||||
|
||||
.login-layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background-color: var(--bg-color);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
background-color: var(--white);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 3rem;
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.08);
|
||||
animation: slideUp 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
height: 52px;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
font-size: 2.33rem;
|
||||
font-weight: 900;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.login-selection {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.role-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 2rem 1.5rem;
|
||||
border: 2px solid var(--bg-light);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
|
||||
.role-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
background-color: var(--white);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 20px rgba(30, 81, 73, 0.08);
|
||||
}
|
||||
|
||||
.role-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background-color: var(--white);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.25rem;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.role-card:hover .role-icon {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--white);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.role-card h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.role-card p {
|
||||
font-size: 1.08rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: 3rem;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,83 +1,83 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openSwDashboardDetail, openSwUsageDetail } from '../../components/Modal/DashboardDetailModal';
|
||||
import { normalizeDate } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
|
||||
export function renderSwDashboard(container: HTMLElement) {
|
||||
let extQty = 0, extUsed = 0, extExp = 0, extTotal = 0;
|
||||
let intQty = 0, intUsed = 0, intExp = 0, intTotal = 0;
|
||||
|
||||
let extCost2026 = 0;
|
||||
let intCost2026 = 0;
|
||||
|
||||
// 통합 SW 데이터
|
||||
const allSw = [...state.masterData.swExternal, ...state.masterData.swInternal];
|
||||
|
||||
allSw.forEach((sw: any) => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length;
|
||||
const qty = typeof sw[ASSET_SCHEMA.ASSET_COUNT.key] === 'number' ? sw[ASSET_SCHEMA.ASSET_COUNT.key] : parseInt(sw[ASSET_SCHEMA.ASSET_COUNT.key]||'0', 10);
|
||||
const priceStr = sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? String(sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/,/g, '') : '0';
|
||||
const price = parseInt(priceStr, 10) || 0;
|
||||
|
||||
if (sw.asset_type === '외부SW') {
|
||||
extQty += qty; extUsed += assigned; extTotal++;
|
||||
if (isSWExpiring(sw)) extExp++;
|
||||
if (sw[ASSET_SCHEMA.PURCHASE_DATE.key]?.startsWith('2026')) extCost2026 += price;
|
||||
} else {
|
||||
intQty += qty; intUsed += assigned; intTotal++;
|
||||
if (sw[ASSET_SCHEMA.PURCHASE_DATE.key]?.startsWith('2026')) intCost2026 += price;
|
||||
}
|
||||
});
|
||||
|
||||
const extPer = extQty > 0 ? Math.round((extUsed/extQty)*100) : 0;
|
||||
const intPer = intQty > 0 ? Math.round((intUsed/intQty)*100) : 0;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="view-container bg-soft">
|
||||
<h3 class="dashboard-section-title">소프트웨어 라이선스 현황</h3>
|
||||
|
||||
<div class="dashboard-layout-2col mb-6">
|
||||
<div class="dashboard-card clickable" data-action="ext-usage">
|
||||
<div class="stat-label">외부 소프트웨어 사용율</div>
|
||||
<div class="stat-sub">${extQty}카피 중 ${extUsed}개 할당</div>
|
||||
<div class="stat-value text-primary">${extPer}%</div>
|
||||
<div class="stat-progress-bar">
|
||||
<div class="progress-fill" style="width: ${extPer}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card clickable" data-action="int-usage">
|
||||
<div class="stat-label">내부 소프트웨어 현황</div>
|
||||
<div class="stat-sub">등록된 내부 솔루션: ${intTotal}개</div>
|
||||
<div class="stat-value text-primary">${intPer}%</div>
|
||||
<div class="stat-progress-bar">
|
||||
<div class="progress-fill" style="width: ${intPer}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="dashboard-section-title">2026년 누적 도입 비용 분석</h3>
|
||||
|
||||
<div class="dashboard-layout-2col">
|
||||
<div class="dashboard-card">
|
||||
<div class="stat-label">외부 SW 누적 비용 (2026)</div>
|
||||
<div class="stat-value text-primary">₩ ${extCost2026.toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="dashboard-card">
|
||||
<div class="stat-label">내부 SW 누적 비용 (2026)</div>
|
||||
<div class="stat-value text-blue">₩ ${intCost2026.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.querySelector('[data-action="ext-usage"]')?.addEventListener('click', () => openSwUsageDetail('외부 소프트웨어 사용 목록', state.masterData.swExternal));
|
||||
container.querySelector('[data-action="int-usage"]')?.addEventListener('click', () => openSwUsageDetail('내부 소프트웨어 사용 목록', state.masterData.swInternal));
|
||||
}
|
||||
|
||||
function isSWExpiring(sw: any) {
|
||||
const expiry = sw[ASSET_SCHEMA.EXPIRED_DATE.key];
|
||||
if (!expiry) return false;
|
||||
const endMs = new Date(normalizeDate(expiry)).getTime();
|
||||
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
|
||||
return diffDays >= 0 && diffDays <= 30;
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openSwDashboardDetail, openSwUsageDetail } from '../../components/Modal/DashboardDetailModal';
|
||||
import { normalizeDate } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
|
||||
export function renderSwDashboard(container: HTMLElement) {
|
||||
let extQty = 0, extUsed = 0, extExp = 0, extTotal = 0;
|
||||
let intQty = 0, intUsed = 0, intExp = 0, intTotal = 0;
|
||||
|
||||
let extCost2026 = 0;
|
||||
let intCost2026 = 0;
|
||||
|
||||
// 통합 SW 데이터
|
||||
const allSw = [...state.masterData.swExternal, ...state.masterData.swInternal];
|
||||
|
||||
allSw.forEach((sw: any) => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.sw_id === sw.id).length;
|
||||
const qty = typeof sw[ASSET_SCHEMA.ASSET_COUNT.key] === 'number' ? sw[ASSET_SCHEMA.ASSET_COUNT.key] : parseInt(sw[ASSET_SCHEMA.ASSET_COUNT.key]||'0', 10);
|
||||
const priceStr = sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? String(sw[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/,/g, '') : '0';
|
||||
const price = parseInt(priceStr, 10) || 0;
|
||||
|
||||
if (sw.asset_type === '외부SW') {
|
||||
extQty += qty; extUsed += assigned; extTotal++;
|
||||
if (isSWExpiring(sw)) extExp++;
|
||||
if (sw[ASSET_SCHEMA.PURCHASE_DATE.key]?.startsWith('2026')) extCost2026 += price;
|
||||
} else {
|
||||
intQty += qty; intUsed += assigned; intTotal++;
|
||||
if (sw[ASSET_SCHEMA.PURCHASE_DATE.key]?.startsWith('2026')) intCost2026 += price;
|
||||
}
|
||||
});
|
||||
|
||||
const extPer = extQty > 0 ? Math.round((extUsed/extQty)*100) : 0;
|
||||
const intPer = intQty > 0 ? Math.round((intUsed/intQty)*100) : 0;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="view-container bg-soft">
|
||||
<h3 class="dashboard-section-title">소프트웨어 라이선스 현황</h3>
|
||||
|
||||
<div class="dashboard-layout-2col mb-6">
|
||||
<div class="dashboard-card clickable" data-action="ext-usage">
|
||||
<div class="stat-label">외부 소프트웨어 사용율</div>
|
||||
<div class="stat-sub">${extQty}카피 중 ${extUsed}개 할당</div>
|
||||
<div class="stat-value text-primary">${extPer}%</div>
|
||||
<div class="stat-progress-bar">
|
||||
<div class="progress-fill" style="width: ${extPer}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card clickable" data-action="int-usage">
|
||||
<div class="stat-label">내부 소프트웨어 현황</div>
|
||||
<div class="stat-sub">등록된 내부 솔루션: ${intTotal}개</div>
|
||||
<div class="stat-value text-primary">${intPer}%</div>
|
||||
<div class="stat-progress-bar">
|
||||
<div class="progress-fill" style="width: ${intPer}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="dashboard-section-title">2026년 누적 도입 비용 분석</h3>
|
||||
|
||||
<div class="dashboard-layout-2col">
|
||||
<div class="dashboard-card">
|
||||
<div class="stat-label">외부 SW 누적 비용 (2026)</div>
|
||||
<div class="stat-value text-primary">₩ ${extCost2026.toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="dashboard-card">
|
||||
<div class="stat-label">내부 SW 누적 비용 (2026)</div>
|
||||
<div class="stat-value text-blue">₩ ${intCost2026.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.querySelector('[data-action="ext-usage"]')?.addEventListener('click', () => openSwUsageDetail('외부 소프트웨어 사용 목록', state.masterData.swExternal));
|
||||
container.querySelector('[data-action="int-usage"]')?.addEventListener('click', () => openSwUsageDetail('내부 소프트웨어 사용 목록', state.masterData.swInternal));
|
||||
}
|
||||
|
||||
function isSWExpiring(sw: any) {
|
||||
const expiry = sw[ASSET_SCHEMA.EXPIRED_DATE.key];
|
||||
if (!expiry) return false;
|
||||
const endMs = new Date(normalizeDate(expiry)).getTime();
|
||||
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
|
||||
return diffDays >= 0 && diffDays <= 30;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,37 +1,37 @@
|
||||
import { state } from '../core/state';
|
||||
import { renderHwDashboard } from './Dashboard/HwDashboard';
|
||||
import { renderSwDashboard } from './Dashboard/SwDashboard';
|
||||
import './Dashboard/dashboard.css';
|
||||
|
||||
/**
|
||||
* 대시보드 렌더링 통합 허브 (Vercel Style Normalized)
|
||||
*/
|
||||
export function renderDashboard(mainContent: HTMLElement) {
|
||||
if (!mainContent) return;
|
||||
|
||||
// 기존 차트 리소스 해제
|
||||
if (state.activeCharts) {
|
||||
state.activeCharts.forEach((c: any) => {
|
||||
if (c && typeof c.destroy === 'function') c.destroy();
|
||||
});
|
||||
}
|
||||
state.activeCharts = [];
|
||||
|
||||
mainContent.innerHTML = `
|
||||
<div class="view-content-wrapper">
|
||||
<div id="dashboard-scroll-container" class="table-container" style="padding: 0;">
|
||||
<div id="dashboard-inner-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const innerContent = document.getElementById('dashboard-inner-content')!;
|
||||
|
||||
if (state.activeCategory === 'hw') {
|
||||
renderHwDashboard(innerContent);
|
||||
} else if (state.activeCategory === 'sw') {
|
||||
renderSwDashboard(innerContent);
|
||||
} else {
|
||||
innerContent.innerHTML = `<div class="dashboard-section-title" style="padding:2rem;">해당 카테고리의 대시보드는 준비 중입니다.</div>`;
|
||||
}
|
||||
}
|
||||
import { state } from '../core/state';
|
||||
import { renderHwDashboard } from './Dashboard/HwDashboard';
|
||||
import { renderSwDashboard } from './Dashboard/SwDashboard';
|
||||
import './Dashboard/dashboard.css';
|
||||
|
||||
/**
|
||||
* 대시보드 렌더링 통합 허브 (Vercel Style Normalized)
|
||||
*/
|
||||
export function renderDashboard(mainContent: HTMLElement) {
|
||||
if (!mainContent) return;
|
||||
|
||||
// 기존 차트 리소스 해제
|
||||
if (state.activeCharts) {
|
||||
state.activeCharts.forEach((c: any) => {
|
||||
if (c && typeof c.destroy === 'function') c.destroy();
|
||||
});
|
||||
}
|
||||
state.activeCharts = [];
|
||||
|
||||
mainContent.innerHTML = `
|
||||
<div class="view-content-wrapper">
|
||||
<div id="dashboard-scroll-container" class="table-container" style="padding: 0;">
|
||||
<div id="dashboard-inner-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const innerContent = document.getElementById('dashboard-inner-content')!;
|
||||
|
||||
if (state.activeCategory === 'hw') {
|
||||
renderHwDashboard(innerContent);
|
||||
} else if (state.activeCategory === 'sw') {
|
||||
renderSwDashboard(innerContent);
|
||||
} else {
|
||||
innerContent.innerHTML = `<div class="dashboard-section-title" style="padding:2rem;">해당 카테고리의 대시보드는 준비 중입니다.</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openSwModal } from '../../components/Modal/SWModal';
|
||||
import { formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderCloudList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '클라우드',
|
||||
dataSource: () => state.masterData.cloud || [],
|
||||
searchKeys: ['PRODUCT_NAME', 'ASSET_PURPOSE', 'PURCHASE_VENDOR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.PURCHASE_VENDOR.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openSwModal(asset, 'view'),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.PRODUCT_NAME.ui, sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => a[ASSET_SCHEMA.PRODUCT_NAME.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, render: a => a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.PURCHASE_VENDOR.ui, sortKey: ASSET_SCHEMA.PURCHASE_VENDOR.key, render: a => a[ASSET_SCHEMA.PURCHASE_VENDOR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{
|
||||
header: ASSET_SCHEMA.PURCHASE_AMOUNT.ui,
|
||||
sortKey: ASSET_SCHEMA.PURCHASE_AMOUNT.key,
|
||||
align: 'right',
|
||||
render: a => `<span style="font-weight:600;">₩ ${a[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? Number(String(a[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/,/g, '')).toLocaleString() : '0'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openSwModal } from '../../components/Modal/SWModal';
|
||||
import { formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderCloudList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '클라우드',
|
||||
dataSource: () => state.masterData.cloud || [],
|
||||
searchKeys: ['PRODUCT_NAME', 'ASSET_PURPOSE', 'PURCHASE_VENDOR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.PURCHASE_VENDOR.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openSwModal(asset, 'view'),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.PRODUCT_NAME.ui, sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => a[ASSET_SCHEMA.PRODUCT_NAME.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, render: a => a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.PURCHASE_VENDOR.ui, sortKey: ASSET_SCHEMA.PURCHASE_VENDOR.key, render: a => a[ASSET_SCHEMA.PURCHASE_VENDOR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{
|
||||
header: ASSET_SCHEMA.PURCHASE_AMOUNT.ui,
|
||||
sortKey: ASSET_SCHEMA.PURCHASE_AMOUNT.key,
|
||||
align: 'right',
|
||||
render: a => `<span style="font-weight:600;">₩ ${a[ASSET_SCHEMA.PURCHASE_AMOUNT.key] ? Number(String(a[ASSET_SCHEMA.PURCHASE_AMOUNT.key]).replace(/,/g, '')).toLocaleString() : '0'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import { state } from '../../core/state';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderCostList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '비용관리',
|
||||
dataSource: () => sortAssets(state.masterData.cloud?.filter((a: any) => a.category === '비용관리') || []),
|
||||
searchKeys: ['PRODUCT_NAME', 'MANAGER_MAIN', 'EMAIL_ACCOUNT', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: () => alert('상세 정보 준비 중입니다.'),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, render: a => formatInline(a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '-') },
|
||||
{ header: '현 사용자', sortKey: ASSET_SCHEMA.MANAGER_MAIN.key, align: 'center', render: a => a[ASSET_SCHEMA.MANAGER_MAIN.key] || '-' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.EMAIL_ACCOUNT.ui, sortKey: ASSET_SCHEMA.EMAIL_ACCOUNT.key, render: a => a[ASSET_SCHEMA.EMAIL_ACCOUNT.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderCostList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '비용관리',
|
||||
dataSource: () => sortAssets(state.masterData.cloud?.filter((a: any) => a.category === '비용관리') || []),
|
||||
searchKeys: ['PRODUCT_NAME', 'MANAGER_MAIN', 'EMAIL_ACCOUNT', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: () => alert('상세 정보 준비 중입니다.'),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, render: a => formatInline(a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '-') },
|
||||
{ header: '현 사용자', sortKey: ASSET_SCHEMA.MANAGER_MAIN.key, align: 'center', render: a => a[ASSET_SCHEMA.MANAGER_MAIN.key] || '-' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.EMAIL_ACCOUNT.ui, sortKey: ASSET_SCHEMA.EMAIL_ACCOUNT.key, render: a => a[ASSET_SCHEMA.EMAIL_ACCOUNT.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openDomainModal } from '../../components/Modal/DomainModal';
|
||||
import { formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { SortState } from '../../core/tableHandler';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
// 정렬 상태를 모듈 수준에서 관리하여 화면 갱신 시에도 유지되도록 함
|
||||
let persistentSortState: SortState = { key: '', direction: 'asc' };
|
||||
|
||||
export function renderDomainList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '도메인',
|
||||
dataSource: () => state.masterData.domain || [],
|
||||
searchKeys: ['DOMAIN_ADDR', 'ASSET_PURPOSE', 'PRODUCT_NAME', 'ASSET_TYPE'],
|
||||
persistentSortState,
|
||||
emptyMessage: '등록된 도메인 정보가 없습니다.',
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.DOMAIN_ADDR.ui}/${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (item) => openDomainModal(item),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.DOMAIN_ADDR.ui, sortKey: ASSET_SCHEMA.DOMAIN_ADDR.key, align: 'left', render: a => a[ASSET_SCHEMA.DOMAIN_ADDR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, align: 'left', render: a => a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.PURCHASE_CORP.ui, sortKey: ASSET_SCHEMA.PURCHASE_CORP.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_CORP.key] || '' },
|
||||
{ header: ASSET_SCHEMA.EXPIRED_DATE.ui, sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openDomainModal } from '../../components/Modal/DomainModal';
|
||||
import { formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { SortState } from '../../core/tableHandler';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
// 정렬 상태를 모듈 수준에서 관리하여 화면 갱신 시에도 유지되도록 함
|
||||
let persistentSortState: SortState = { key: '', direction: 'asc' };
|
||||
|
||||
export function renderDomainList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '도메인',
|
||||
dataSource: () => state.masterData.domain || [],
|
||||
searchKeys: ['DOMAIN_ADDR', 'ASSET_PURPOSE', 'PRODUCT_NAME', 'ASSET_TYPE'],
|
||||
persistentSortState,
|
||||
emptyMessage: '등록된 도메인 정보가 없습니다.',
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.DOMAIN_ADDR.ui}/${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (item) => openDomainModal(item),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.DOMAIN_ADDR.ui, sortKey: ASSET_SCHEMA.DOMAIN_ADDR.key, align: 'left', render: a => a[ASSET_SCHEMA.DOMAIN_ADDR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, align: 'left', render: a => a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.PURCHASE_CORP.ui, sortKey: ASSET_SCHEMA.PURCHASE_CORP.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_CORP.key] || '' },
|
||||
{ header: ASSET_SCHEMA.EXPIRED_DATE.ui, sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderEquipmentList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '업무지원장비',
|
||||
dataSource: () => sortAssets(state.masterData.equipment || []),
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
render: a => `<span class="badge badge-${a[ASSET_SCHEMA.HW_STATUS.key] === '대여중' ? 'primary' : 'success'}">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a.명칭 || '-') },
|
||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderEquipmentList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '업무지원장비',
|
||||
dataSource: () => sortAssets(state.masterData.equipment || []),
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
render: a => `<span class="badge badge-${a[ASSET_SCHEMA.HW_STATUS.key] === '대여중' ? 'primary' : 'success'}">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a.명칭 || '-') },
|
||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderFacilityList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '사무가구',
|
||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '시설자산') || []),
|
||||
searchKeys: ['MODEL_NAME', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderFacilityList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '사무가구',
|
||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '시설자산') || []),
|
||||
searchKeys: ['MODEL_NAME', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { state } from '../../core/state';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderGiftList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '선물',
|
||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '선물') || []),
|
||||
searchKeys: ['PRODUCT_NAME', 'MODEL_NAME', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: () => alert('상세 정보 준비 중입니다.'),
|
||||
columns: [
|
||||
{ header: '자산명', sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.PRODUCT_NAME.key] || a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: '구매연월', sortKey: ASSET_SCHEMA.PURCHASE_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.EXPIRED_DATE.ui, sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderGiftList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '선물',
|
||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '선물') || []),
|
||||
searchKeys: ['PRODUCT_NAME', 'MODEL_NAME', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: () => alert('상세 정보 준비 중입니다.'),
|
||||
columns: [
|
||||
{ header: '자산명', sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.PRODUCT_NAME.key] || a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: '구매연월', sortKey: ASSET_SCHEMA.PURCHASE_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.EXPIRED_DATE.ui, sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,40 +1,40 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderMobileList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: 'PC', // Legacy support
|
||||
dataSource: () => sortAssets(state.masterData.mobile || []),
|
||||
searchKeys: ['MODEL_NAME', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.HW_STATUS.ui, sortKey: ASSET_SCHEMA.HW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.HW_STATUS.key] || '운영중' },
|
||||
{ header: ASSET_SCHEMA.PURCHASE_CORP.ui, sortKey: ASSET_SCHEMA.PURCHASE_CORP.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_CORP.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => a[ASSET_SCHEMA.MODEL_NAME.key] || '' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.PURCHASE_DATE.ui, sortKey: ASSET_SCHEMA.PURCHASE_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.PURCHASE_AMOUNT.ui, sortKey: ASSET_SCHEMA.PURCHASE_AMOUNT.key, align: 'right', render: a => Number(a[ASSET_SCHEMA.PURCHASE_AMOUNT.key]||0).toLocaleString() },
|
||||
{ header: '담당자', align: 'center', render: a => a[ASSET_SCHEMA.MANAGER_MAIN.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderMobileList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: 'PC', // Legacy support
|
||||
dataSource: () => sortAssets(state.masterData.mobile || []),
|
||||
searchKeys: ['MODEL_NAME', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.HW_STATUS.ui, sortKey: ASSET_SCHEMA.HW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.HW_STATUS.key] || '운영중' },
|
||||
{ header: ASSET_SCHEMA.PURCHASE_CORP.ui, sortKey: ASSET_SCHEMA.PURCHASE_CORP.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_CORP.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => a[ASSET_SCHEMA.MODEL_NAME.key] || '' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.PURCHASE_DATE.ui, sortKey: ASSET_SCHEMA.PURCHASE_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.PURCHASE_AMOUNT.ui, sortKey: ASSET_SCHEMA.PURCHASE_AMOUNT.key, align: 'right', render: a => Number(a[ASSET_SCHEMA.PURCHASE_AMOUNT.key]||0).toLocaleString() },
|
||||
{ header: '담당자', align: 'center', render: a => a[ASSET_SCHEMA.MANAGER_MAIN.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderNetworkList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '네트워크',
|
||||
dataSource: () => sortAssets(state.masterData.network || []),
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderNetworkList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '네트워크',
|
||||
dataSource: () => sortAssets(state.masterData.network || []),
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,174 +1,174 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openPartsMasterModal } from '../../components/Modal/PartsMasterModal';
|
||||
import { openJobSpecModal } from '../../components/Modal/JobSpecModal';
|
||||
import { formatInline } from '../../core/utils';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export let activePartsMasterSubTab: 'parts-master' | 'job-spec' = 'parts-master';
|
||||
|
||||
export function renderPartsMasterList(container: HTMLElement) {
|
||||
if (activePartsMasterSubTab === 'parts-master') {
|
||||
createListView(container, {
|
||||
title: '부품 마스터',
|
||||
dataSource: () => state.masterData.partsMaster || [],
|
||||
searchKeys: ['component_name', 'category', 'score_tier'],
|
||||
filterOptions: {
|
||||
keywordLabel: '부품명 / 등급 검색',
|
||||
showLoc: false,
|
||||
showDept: false,
|
||||
showType: false
|
||||
},
|
||||
onRowClick: (component) => openPartsMasterModal(component, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: 'ID',
|
||||
sortKey: 'id',
|
||||
align: 'center',
|
||||
width: '5%',
|
||||
render: c => c.id.toString()
|
||||
},
|
||||
{
|
||||
header: '분류',
|
||||
sortKey: 'category',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: c => {
|
||||
let badgeClass = 'badge-primary';
|
||||
if (c.category === 'CPU') badgeClass = 'badge-primary';
|
||||
else if (c.category === 'GPU') badgeClass = 'badge-success';
|
||||
else if (c.category === 'RAM') badgeClass = 'badge-warning';
|
||||
return `<span class="badge ${badgeClass}">${c.category}</span>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
header: '부품 표준 명칭',
|
||||
sortKey: 'component_name',
|
||||
render: c => formatInline(c.component_name || '-')
|
||||
},
|
||||
{
|
||||
header: '성능 등급',
|
||||
sortKey: 'score_tier',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: c => c.score_tier || '-'
|
||||
},
|
||||
{
|
||||
header: '감점 점수',
|
||||
sortKey: 'deduction',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: c => {
|
||||
const score = c.deduction || 0;
|
||||
let color = '#3b82f6'; // blue
|
||||
if (score >= 20) color = '#ef4444'; // red
|
||||
else if (score >= 10) color = '#f59e0b'; // orange
|
||||
return `<strong style="color: ${color}; font-size: 14px;">-${score}점</strong>`;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
createListView(container, {
|
||||
title: '직무별 기준 사양',
|
||||
dataSource: () => state.masterData.jobSpecs || [],
|
||||
searchKeys: ['job_name', 'cpu_standard', 'ram_standard', 'gpu_standard', 'remarks'],
|
||||
filterOptions: {
|
||||
keywordLabel: '직무명 / 사양 검색',
|
||||
showLoc: false,
|
||||
showDept: false,
|
||||
showType: false
|
||||
},
|
||||
onRowClick: (jobSpec) => openJobSpecModal(jobSpec, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: 'ID',
|
||||
sortKey: 'id',
|
||||
align: 'center',
|
||||
width: '5%',
|
||||
render: j => j.id.toString()
|
||||
},
|
||||
{
|
||||
header: '직무명',
|
||||
sortKey: 'job_name',
|
||||
width: '15%',
|
||||
render: j => `<strong style="color: var(--primary-color); font-size: 14px;">${formatInline(j.job_name || '-')}</strong>`
|
||||
},
|
||||
{
|
||||
header: '권장 CPU 사양',
|
||||
sortKey: 'cpu_standard',
|
||||
render: j => formatInline(j.cpu_standard || '-')
|
||||
},
|
||||
{
|
||||
header: '권장 RAM 사양',
|
||||
sortKey: 'ram_standard',
|
||||
width: '12%',
|
||||
render: j => formatInline(j.ram_standard || '-')
|
||||
},
|
||||
{
|
||||
header: '권장 GPU 사양',
|
||||
sortKey: 'gpu_standard',
|
||||
render: j => formatInline(j.gpu_standard || '-')
|
||||
},
|
||||
{
|
||||
header: '기준 점수',
|
||||
sortKey: 'min_score',
|
||||
align: 'center',
|
||||
width: '10%',
|
||||
render: j => `<span style="font-weight: 700;">${j.min_score || 0}점 이상</span>`
|
||||
},
|
||||
{
|
||||
header: '비고',
|
||||
sortKey: 'remarks',
|
||||
width: '20%',
|
||||
render: j => formatInline(j.remarks || '-')
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
renderSubTabs(container);
|
||||
}
|
||||
|
||||
function renderSubTabs(container: HTMLElement) {
|
||||
const header = container.querySelector('.page-header');
|
||||
if (!header) return;
|
||||
|
||||
// 기존에 생성된 탭 바가 있다면 제거하여 중복 방지 (스타일만 수정하는 최소 침습 방식)
|
||||
const existingTabs = container.querySelector('.sub-tab-container');
|
||||
if (existingTabs) existingTabs.remove();
|
||||
|
||||
const tabContainer = document.createElement('div');
|
||||
tabContainer.className = 'sub-tab-container';
|
||||
tabContainer.style.cssText = 'display: flex; gap: 1rem; padding: 0 2rem; border-bottom: 1px solid var(--hairline); background: var(--canvas);';
|
||||
|
||||
const tab1Active = activePartsMasterSubTab === 'parts-master';
|
||||
const tab2Active = activePartsMasterSubTab === 'job-spec';
|
||||
|
||||
tabContainer.innerHTML = `
|
||||
<button id="tab-parts-master" class="sub-tab-btn ${tab1Active ? 'active' : ''}" style="padding: 1rem 0.5rem; border: none; background: none; font-size: var(--fs-sm); font-weight: 600; cursor: pointer; color: ${tab1Active ? 'var(--primary)' : 'var(--mute)'}; position: relative; border-bottom: 2px solid ${tab1Active ? 'var(--primary)' : 'transparent'}; margin-bottom: -1px;">
|
||||
부품 표준 등급
|
||||
</button>
|
||||
<button id="tab-job-spec" class="sub-tab-btn ${tab2Active ? 'active' : ''}" style="padding: 1rem 0.5rem; border: none; background: none; font-size: var(--fs-sm); font-weight: 600; cursor: pointer; color: ${tab2Active ? 'var(--primary)' : 'var(--mute)'}; position: relative; border-bottom: 2px solid ${tab2Active ? 'var(--primary)' : 'transparent'}; margin-bottom: -1px;">
|
||||
직무별 기준 사양
|
||||
</button>
|
||||
`;
|
||||
|
||||
header.parentNode!.insertBefore(tabContainer, header.nextSibling);
|
||||
|
||||
const tabPartsMaster = tabContainer.querySelector('#tab-parts-master')!;
|
||||
const tabJobSpec = tabContainer.querySelector('#tab-job-spec')!;
|
||||
|
||||
tabPartsMaster.addEventListener('click', () => {
|
||||
if (activePartsMasterSubTab !== 'parts-master') {
|
||||
activePartsMasterSubTab = 'parts-master';
|
||||
renderPartsMasterList(container);
|
||||
}
|
||||
});
|
||||
|
||||
tabJobSpec.addEventListener('click', () => {
|
||||
if (activePartsMasterSubTab !== 'job-spec') {
|
||||
activePartsMasterSubTab = 'job-spec';
|
||||
renderPartsMasterList(container);
|
||||
}
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openPartsMasterModal } from '../../components/Modal/PartsMasterModal';
|
||||
import { openJobSpecModal } from '../../components/Modal/JobSpecModal';
|
||||
import { formatInline } from '../../core/utils';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export let activePartsMasterSubTab: 'parts-master' | 'job-spec' = 'parts-master';
|
||||
|
||||
export function renderPartsMasterList(container: HTMLElement) {
|
||||
if (activePartsMasterSubTab === 'parts-master') {
|
||||
createListView(container, {
|
||||
title: '부품 마스터',
|
||||
dataSource: () => state.masterData.partsMaster || [],
|
||||
searchKeys: ['component_name', 'category', 'score_tier'],
|
||||
filterOptions: {
|
||||
keywordLabel: '부품명 / 등급 검색',
|
||||
showLoc: false,
|
||||
showDept: false,
|
||||
showType: false
|
||||
},
|
||||
onRowClick: (component) => openPartsMasterModal(component, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: 'ID',
|
||||
sortKey: 'id',
|
||||
align: 'center',
|
||||
width: '5%',
|
||||
render: c => c.id.toString()
|
||||
},
|
||||
{
|
||||
header: '분류',
|
||||
sortKey: 'category',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: c => {
|
||||
let badgeClass = 'badge-primary';
|
||||
if (c.category === 'CPU') badgeClass = 'badge-primary';
|
||||
else if (c.category === 'GPU') badgeClass = 'badge-success';
|
||||
else if (c.category === 'RAM') badgeClass = 'badge-warning';
|
||||
return `<span class="badge ${badgeClass}">${c.category}</span>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
header: '부품 표준 명칭',
|
||||
sortKey: 'component_name',
|
||||
render: c => formatInline(c.component_name || '-')
|
||||
},
|
||||
{
|
||||
header: '성능 등급',
|
||||
sortKey: 'score_tier',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: c => c.score_tier || '-'
|
||||
},
|
||||
{
|
||||
header: '감점 점수',
|
||||
sortKey: 'deduction',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: c => {
|
||||
const score = c.deduction || 0;
|
||||
let color = '#3b82f6'; // blue
|
||||
if (score >= 20) color = '#ef4444'; // red
|
||||
else if (score >= 10) color = '#f59e0b'; // orange
|
||||
return `<strong style="color: ${color}; font-size: 14px;">-${score}점</strong>`;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
createListView(container, {
|
||||
title: '직무별 기준 사양',
|
||||
dataSource: () => state.masterData.jobSpecs || [],
|
||||
searchKeys: ['job_name', 'cpu_standard', 'ram_standard', 'gpu_standard', 'remarks'],
|
||||
filterOptions: {
|
||||
keywordLabel: '직무명 / 사양 검색',
|
||||
showLoc: false,
|
||||
showDept: false,
|
||||
showType: false
|
||||
},
|
||||
onRowClick: (jobSpec) => openJobSpecModal(jobSpec, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: 'ID',
|
||||
sortKey: 'id',
|
||||
align: 'center',
|
||||
width: '5%',
|
||||
render: j => j.id.toString()
|
||||
},
|
||||
{
|
||||
header: '직무명',
|
||||
sortKey: 'job_name',
|
||||
width: '15%',
|
||||
render: j => `<strong style="color: var(--primary-color); font-size: 14px;">${formatInline(j.job_name || '-')}</strong>`
|
||||
},
|
||||
{
|
||||
header: '권장 CPU 사양',
|
||||
sortKey: 'cpu_standard',
|
||||
render: j => formatInline(j.cpu_standard || '-')
|
||||
},
|
||||
{
|
||||
header: '권장 RAM 사양',
|
||||
sortKey: 'ram_standard',
|
||||
width: '12%',
|
||||
render: j => formatInline(j.ram_standard || '-')
|
||||
},
|
||||
{
|
||||
header: '권장 GPU 사양',
|
||||
sortKey: 'gpu_standard',
|
||||
render: j => formatInline(j.gpu_standard || '-')
|
||||
},
|
||||
{
|
||||
header: '기준 점수',
|
||||
sortKey: 'min_score',
|
||||
align: 'center',
|
||||
width: '10%',
|
||||
render: j => `<span style="font-weight: 700;">${j.min_score || 0}점 이상</span>`
|
||||
},
|
||||
{
|
||||
header: '비고',
|
||||
sortKey: 'remarks',
|
||||
width: '20%',
|
||||
render: j => formatInline(j.remarks || '-')
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
renderSubTabs(container);
|
||||
}
|
||||
|
||||
function renderSubTabs(container: HTMLElement) {
|
||||
const header = container.querySelector('.page-header');
|
||||
if (!header) return;
|
||||
|
||||
// 기존에 생성된 탭 바가 있다면 제거하여 중복 방지 (스타일만 수정하는 최소 침습 방식)
|
||||
const existingTabs = container.querySelector('.sub-tab-container');
|
||||
if (existingTabs) existingTabs.remove();
|
||||
|
||||
const tabContainer = document.createElement('div');
|
||||
tabContainer.className = 'sub-tab-container';
|
||||
tabContainer.style.cssText = 'display: flex; gap: 1rem; padding: 0 2rem; border-bottom: 1px solid var(--hairline); background: var(--canvas);';
|
||||
|
||||
const tab1Active = activePartsMasterSubTab === 'parts-master';
|
||||
const tab2Active = activePartsMasterSubTab === 'job-spec';
|
||||
|
||||
tabContainer.innerHTML = `
|
||||
<button id="tab-parts-master" class="sub-tab-btn ${tab1Active ? 'active' : ''}" style="padding: 1rem 0.5rem; border: none; background: none; font-size: var(--fs-sm); font-weight: 600; cursor: pointer; color: ${tab1Active ? 'var(--primary)' : 'var(--mute)'}; position: relative; border-bottom: 2px solid ${tab1Active ? 'var(--primary)' : 'transparent'}; margin-bottom: -1px;">
|
||||
부품 표준 등급
|
||||
</button>
|
||||
<button id="tab-job-spec" class="sub-tab-btn ${tab2Active ? 'active' : ''}" style="padding: 1rem 0.5rem; border: none; background: none; font-size: var(--fs-sm); font-weight: 600; cursor: pointer; color: ${tab2Active ? 'var(--primary)' : 'var(--mute)'}; position: relative; border-bottom: 2px solid ${tab2Active ? 'var(--primary)' : 'transparent'}; margin-bottom: -1px;">
|
||||
직무별 기준 사양
|
||||
</button>
|
||||
`;
|
||||
|
||||
header.parentNode!.insertBefore(tabContainer, header.nextSibling);
|
||||
|
||||
const tabPartsMaster = tabContainer.querySelector('#tab-parts-master')!;
|
||||
const tabJobSpec = tabContainer.querySelector('#tab-job-spec')!;
|
||||
|
||||
tabPartsMaster.addEventListener('click', () => {
|
||||
if (activePartsMasterSubTab !== 'parts-master') {
|
||||
activePartsMasterSubTab = 'parts-master';
|
||||
renderPartsMasterList(container);
|
||||
}
|
||||
});
|
||||
|
||||
tabJobSpec.addEventListener('click', () => {
|
||||
if (activePartsMasterSubTab !== 'job-spec') {
|
||||
activePartsMasterSubTab = 'job-spec';
|
||||
renderPartsMasterList(container);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,114 +1,114 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline, calculatePcScoreDeductive, getPcGrade, isWindows11Incompatible } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
import { SortState } from '../../core/tableHandler';
|
||||
|
||||
let persistentSortState: SortState = { key: 'updated_at', direction: 'desc' };
|
||||
|
||||
export function renderPcList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: 'PC',
|
||||
persistentSortState,
|
||||
dataSource: () => {
|
||||
const list = (state.masterData.pc || []).filter((a: any) => a.asset_type !== '서버PC');
|
||||
list.forEach((a: any) => {
|
||||
a['_pc_score'] = calculatePcScoreDeductive(a[ASSET_SCHEMA.CPU.key], a[ASSET_SCHEMA.RAM.key], a[ASSET_SCHEMA.GPU.key], a.purchase_date);
|
||||
});
|
||||
// 변경일시(updated_at) 내림차순 정렬 (최신 변경 항목이 맨 위로)
|
||||
return list.sort((a: any, b: any) => {
|
||||
const dateA = a.updated_at || a.created_at || '';
|
||||
const dateB = b.updated_at || b.created_at || '';
|
||||
if (dateA < dateB) return 1;
|
||||
if (dateA > dateB) return -1;
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
searchKeys: ['CURRENT_DEPT', 'CURRENT_USER', 'MODEL_NAME', 'MAC_ADDR', 'MANAGER_MAIN', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true,
|
||||
showStatus: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
width: '8%',
|
||||
render: a => {
|
||||
const status = a[ASSET_SCHEMA.HW_STATUS.key] || '재고';
|
||||
let badgeClass = 'badge-light';
|
||||
if (status === '운영') badgeClass = 'b-green';
|
||||
else if (status === '재고') badgeClass = 'b-yellow';
|
||||
else if (status === '수리') badgeClass = 'b-purple';
|
||||
else if (status === '폐기') badgeClass = 'badge-muted';
|
||||
return `<span class="badge ${badgeClass}">${status}</span>`;
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.USER_POSITION.ui, sortKey: ASSET_SCHEMA.USER_POSITION.key, align: 'center', render: a => a[ASSET_SCHEMA.USER_POSITION.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.CPU.ui, sortKey: ASSET_SCHEMA.CPU.key, align: 'center', render: a => a[ASSET_SCHEMA.CPU.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MAINBOARD.ui, sortKey: ASSET_SCHEMA.MAINBOARD.key, align: 'center', render: a => a[ASSET_SCHEMA.MAINBOARD.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.RAM.ui, sortKey: ASSET_SCHEMA.RAM.key, align: 'center', render: a => a[ASSET_SCHEMA.RAM.key] || '' },
|
||||
{ header: ASSET_SCHEMA.GPU.ui, sortKey: ASSET_SCHEMA.GPU.key, align: 'center', render: a => a[ASSET_SCHEMA.GPU.key] || '-' },
|
||||
{
|
||||
header: 'SSD',
|
||||
align: 'center',
|
||||
width: '8%',
|
||||
render: a => {
|
||||
try {
|
||||
const vols = a.volumes ? (typeof a.volumes === 'string' ? JSON.parse(a.volumes) : a.volumes) : [];
|
||||
if (Array.isArray(vols)) {
|
||||
const ssds = vols.filter((v: any) => v && String(v.type).toUpperCase() === 'SSD');
|
||||
if (ssds.length > 0) {
|
||||
return ssds.map((v: any) => `${v.capacity || ''}${v.unit || 'GB'}`).join(' / ');
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
header: 'HDD',
|
||||
align: 'center',
|
||||
width: '12%',
|
||||
render: a => {
|
||||
try {
|
||||
const vols = a.volumes ? (typeof a.volumes === 'string' ? JSON.parse(a.volumes) : a.volumes) : [];
|
||||
if (Array.isArray(vols)) {
|
||||
const hdds = vols.filter((v: any) => v && String(v.type).toUpperCase() === 'HDD');
|
||||
if (hdds.length > 0) {
|
||||
return hdds.map((v: any) => `${v.capacity || ''}${v.unit || 'GB'}`).join(' / ');
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
header: ASSET_SCHEMA.MAC_ADDR.ui,
|
||||
sortKey: ASSET_SCHEMA.MAC_ADDR.key,
|
||||
align: 'center',
|
||||
render: a => `<span style="font-family:monospace; font-size:11px;">${a[ASSET_SCHEMA.MAC_ADDR.key] || '-'}</span>`
|
||||
},
|
||||
{
|
||||
header: '성능 등급',
|
||||
sortKey: '_pc_score',
|
||||
align: 'center',
|
||||
width: '8%',
|
||||
render: a => {
|
||||
const score = a._pc_score !== undefined ? a._pc_score : calculatePcScoreDeductive(a[ASSET_SCHEMA.CPU.key], a[ASSET_SCHEMA.RAM.key], a[ASSET_SCHEMA.GPU.key], a.purchase_date);
|
||||
const isWin11Incompatible = isWindows11Incompatible(a[ASSET_SCHEMA.CPU.key], a[ASSET_SCHEMA.RAM.key]);
|
||||
const grade = getPcGrade(score, isWin11Incompatible);
|
||||
return `<span class="badge ${grade.class}" title="성능 점수: ${score}점">${grade.name}</span>`;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline, calculatePcScoreDeductive, getPcGrade, isWindows11Incompatible } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
import { SortState } from '../../core/tableHandler';
|
||||
|
||||
let persistentSortState: SortState = { key: 'updated_at', direction: 'desc' };
|
||||
|
||||
export function renderPcList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: 'PC',
|
||||
persistentSortState,
|
||||
dataSource: () => {
|
||||
const list = (state.masterData.pc || []).filter((a: any) => a.asset_type !== '서버PC');
|
||||
list.forEach((a: any) => {
|
||||
a['_pc_score'] = calculatePcScoreDeductive(a[ASSET_SCHEMA.CPU.key], a[ASSET_SCHEMA.RAM.key], a[ASSET_SCHEMA.GPU.key], a.purchase_date);
|
||||
});
|
||||
// 변경일시(updated_at) 내림차순 정렬 (최신 변경 항목이 맨 위로)
|
||||
return list.sort((a: any, b: any) => {
|
||||
const dateA = a.updated_at || a.created_at || '';
|
||||
const dateB = b.updated_at || b.created_at || '';
|
||||
if (dateA < dateB) return 1;
|
||||
if (dateA > dateB) return -1;
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
searchKeys: ['CURRENT_DEPT', 'CURRENT_USER', 'MODEL_NAME', 'MAC_ADDR', 'MANAGER_MAIN', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true,
|
||||
showStatus: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
width: '8%',
|
||||
render: a => {
|
||||
const status = a[ASSET_SCHEMA.HW_STATUS.key] || '재고';
|
||||
let badgeClass = 'badge-light';
|
||||
if (status === '운영') badgeClass = 'b-green';
|
||||
else if (status === '재고') badgeClass = 'b-yellow';
|
||||
else if (status === '수리') badgeClass = 'b-purple';
|
||||
else if (status === '폐기') badgeClass = 'badge-muted';
|
||||
return `<span class="badge ${badgeClass}">${status}</span>`;
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.USER_POSITION.ui, sortKey: ASSET_SCHEMA.USER_POSITION.key, align: 'center', render: a => a[ASSET_SCHEMA.USER_POSITION.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.CPU.ui, sortKey: ASSET_SCHEMA.CPU.key, align: 'center', render: a => a[ASSET_SCHEMA.CPU.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MAINBOARD.ui, sortKey: ASSET_SCHEMA.MAINBOARD.key, align: 'center', render: a => a[ASSET_SCHEMA.MAINBOARD.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.RAM.ui, sortKey: ASSET_SCHEMA.RAM.key, align: 'center', render: a => a[ASSET_SCHEMA.RAM.key] || '' },
|
||||
{ header: ASSET_SCHEMA.GPU.ui, sortKey: ASSET_SCHEMA.GPU.key, align: 'center', render: a => a[ASSET_SCHEMA.GPU.key] || '-' },
|
||||
{
|
||||
header: 'SSD',
|
||||
align: 'center',
|
||||
width: '8%',
|
||||
render: a => {
|
||||
try {
|
||||
const vols = a.volumes ? (typeof a.volumes === 'string' ? JSON.parse(a.volumes) : a.volumes) : [];
|
||||
if (Array.isArray(vols)) {
|
||||
const ssds = vols.filter((v: any) => v && String(v.type).toUpperCase() === 'SSD');
|
||||
if (ssds.length > 0) {
|
||||
return ssds.map((v: any) => `${v.capacity || ''}${v.unit || 'GB'}`).join(' / ');
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
header: 'HDD',
|
||||
align: 'center',
|
||||
width: '12%',
|
||||
render: a => {
|
||||
try {
|
||||
const vols = a.volumes ? (typeof a.volumes === 'string' ? JSON.parse(a.volumes) : a.volumes) : [];
|
||||
if (Array.isArray(vols)) {
|
||||
const hdds = vols.filter((v: any) => v && String(v.type).toUpperCase() === 'HDD');
|
||||
if (hdds.length > 0) {
|
||||
return hdds.map((v: any) => `${v.capacity || ''}${v.unit || 'GB'}`).join(' / ');
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
header: ASSET_SCHEMA.MAC_ADDR.ui,
|
||||
sortKey: ASSET_SCHEMA.MAC_ADDR.key,
|
||||
align: 'center',
|
||||
render: a => `<span style="font-family:monospace; font-size:11px;">${a[ASSET_SCHEMA.MAC_ADDR.key] || '-'}</span>`
|
||||
},
|
||||
{
|
||||
header: '성능 등급',
|
||||
sortKey: '_pc_score',
|
||||
align: 'center',
|
||||
width: '8%',
|
||||
render: a => {
|
||||
const score = a._pc_score !== undefined ? a._pc_score : calculatePcScoreDeductive(a[ASSET_SCHEMA.CPU.key], a[ASSET_SCHEMA.RAM.key], a[ASSET_SCHEMA.GPU.key], a.purchase_date);
|
||||
const isWin11Incompatible = isWindows11Incompatible(a[ASSET_SCHEMA.CPU.key], a[ASSET_SCHEMA.RAM.key]);
|
||||
const grade = getPcGrade(score, isWin11Incompatible);
|
||||
return `<span class="badge ${grade.class}" title="성능 점수: ${score}점">${grade.name}</span>`;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderPcPartList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: 'PC부품',
|
||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === 'PC부품') || []),
|
||||
searchKeys: ['MODEL_NAME', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.VOLUME.ui, sortKey: ASSET_SCHEMA.VOLUME.key, align: 'center', render: a => a[ASSET_SCHEMA.VOLUME.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.MONITOR_INCH.ui, sortKey: ASSET_SCHEMA.MONITOR_INCH.key, align: 'center', render: a => a[ASSET_SCHEMA.MONITOR_INCH.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderPcPartList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: 'PC부품',
|
||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === 'PC부품') || []),
|
||||
searchKeys: ['MODEL_NAME', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.VOLUME.ui, sortKey: ASSET_SCHEMA.VOLUME.key, align: 'center', render: a => a[ASSET_SCHEMA.VOLUME.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.MONITOR_INCH.ui, sortKey: ASSET_SCHEMA.MONITOR_INCH.key, align: 'center', render: a => a[ASSET_SCHEMA.MONITOR_INCH.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderServerList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '서버',
|
||||
dataSource: () => {
|
||||
const serverList = state.masterData.server || [];
|
||||
const serverPcList = (state.masterData.pc || []).filter((a: any) => a.asset_type === '서버PC');
|
||||
return sortAssets([...serverList, ...serverPcList]);
|
||||
},
|
||||
searchKeys: ['CURRENT_DEPT', 'MODEL_NAME', 'ASSET_PURPOSE', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.CURRENT_DEPT.ui}/${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.CURRENT_DEPT.ui, sortKey: ASSET_SCHEMA.CURRENT_DEPT.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_DEPT.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, align: 'center', width: '15%', render: a => formatInline(a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{
|
||||
header: '모델/메인보드',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || a[ASSET_SCHEMA.MAINBOARD.key] || '-')
|
||||
},
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, width: '35%', className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderServerList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '서버',
|
||||
dataSource: () => {
|
||||
const serverList = state.masterData.server || [];
|
||||
const serverPcList = (state.masterData.pc || []).filter((a: any) => a.asset_type === '서버PC');
|
||||
return sortAssets([...serverList, ...serverPcList]);
|
||||
},
|
||||
searchKeys: ['CURRENT_DEPT', 'MODEL_NAME', 'ASSET_PURPOSE', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.CURRENT_DEPT.ui}/${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.CURRENT_DEPT.ui, sortKey: ASSET_SCHEMA.CURRENT_DEPT.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_DEPT.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, align: 'center', width: '15%', render: a => formatInline(a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{
|
||||
header: '모델/메인보드',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || a[ASSET_SCHEMA.MAINBOARD.key] || '-')
|
||||
},
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, width: '35%', className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderSpaceInfoList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '공간정보장비',
|
||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '공간정보장비') || []),
|
||||
searchKeys: ['MODEL_NAME', 'PRODUCT_NAME', 'CURRENT_USER', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_NAME.ui, sortKey: ASSET_SCHEMA.ASSET_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.PRODUCT_NAME.key] || a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderSpaceInfoList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '공간정보장비',
|
||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '공간정보장비') || []),
|
||||
searchKeys: ['MODEL_NAME', 'PRODUCT_NAME', 'CURRENT_USER', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: ASSET_SCHEMA.HW_STATUS.ui,
|
||||
sortKey: ASSET_SCHEMA.HW_STATUS.key,
|
||||
align: 'center',
|
||||
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span>`
|
||||
},
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_NAME.ui, sortKey: ASSET_SCHEMA.ASSET_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.PRODUCT_NAME.key] || a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderStorageList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '스토리지',
|
||||
dataSource: () => sortAssets(state.masterData.storage || []),
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'SERIAL_NUM', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.HW_STATUS.ui, sortKey: ASSET_SCHEMA.HW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.HW_STATUS.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.VOLUME.ui, sortKey: ASSET_SCHEMA.VOLUME.key, align: 'center', render: a => a[ASSET_SCHEMA.VOLUME.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, align: 'center', render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.SERIAL_NUM.ui, sortKey: ASSET_SCHEMA.SERIAL_NUM.key, align: 'center', render: a => a[ASSET_SCHEMA.SERIAL_NUM.key] || '-' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openHwModal } from '../../components/Modal/HWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderStorageList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '스토리지',
|
||||
dataSource: () => sortAssets(state.masterData.storage || []),
|
||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'SERIAL_NUM', 'ASSET_TYPE'],
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||
showLoc: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||
columns: [
|
||||
{ header: ASSET_SCHEMA.HW_STATUS.ui, sortKey: ASSET_SCHEMA.HW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.HW_STATUS.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.VOLUME.ui, sortKey: ASSET_SCHEMA.VOLUME.key, align: 'center', render: a => a[ASSET_SCHEMA.VOLUME.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, align: 'center', render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-') },
|
||||
{ header: ASSET_SCHEMA.SERIAL_NUM.ui, sortKey: ASSET_SCHEMA.SERIAL_NUM.key, align: 'center', render: a => a[ASSET_SCHEMA.SERIAL_NUM.key] || '-' },
|
||||
{
|
||||
header: ASSET_SCHEMA.LOCATION.ui,
|
||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||
align: 'center',
|
||||
render: a => {
|
||||
const loc = a[ASSET_SCHEMA.LOCATION.key] || '';
|
||||
const detail = a[ASSET_SCHEMA.LOC_DETAIL.key] || '';
|
||||
return detail ? `${loc}(${detail})` : (loc || '-');
|
||||
}
|
||||
},
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openSwModal } from '../../components/Modal/SWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderSwList(container: HTMLElement) {
|
||||
const isInternal = state.activeSubTab === '내부SW';
|
||||
|
||||
createListView(container, {
|
||||
title: isInternal ? '내부SW' : '외부SW',
|
||||
dataSource: () => sortAssets(isInternal ? state.masterData.swInternal : state.masterData.swExternal),
|
||||
searchKeys: ['PRODUCT_NAME', 'CURRENT_USER', 'CURRENT_DEPT', 'ASSET_TYPE'],
|
||||
emptyMessage: '검색 결과가 없습니다.',
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.CURRENT_DEPT.ui})`,
|
||||
showField: true,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openSwModal(asset, 'view'),
|
||||
columns: isInternal ? [
|
||||
{ header: ASSET_SCHEMA.SW_FIELD.ui, sortKey: ASSET_SCHEMA.SW_FIELD.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_FIELD.key] || '' },
|
||||
{ header: ASSET_SCHEMA.DEV_OBJ.ui, sortKey: ASSET_SCHEMA.DEV_OBJ.key, align: 'center', render: a => a[ASSET_SCHEMA.DEV_OBJ.key] || '' },
|
||||
{ header: ASSET_SCHEMA.SW_STATUS.ui, sortKey: ASSET_SCHEMA.SW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_STATUS.key] || '보유중' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.SW_TYPE.ui, sortKey: ASSET_SCHEMA.SW_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_TYPE.key] || '내부' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
] : [
|
||||
{ header: '자산명', sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => a[ASSET_SCHEMA.PRODUCT_NAME.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.SW_STATUS.ui, sortKey: ASSET_SCHEMA.SW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_STATUS.key] || '사용중' },
|
||||
{ header: ASSET_SCHEMA.SW_FIELD.ui, sortKey: ASSET_SCHEMA.SW_FIELD.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_FIELD.key] || '' },
|
||||
{ header: ASSET_SCHEMA.CURRENT_DEPT.ui, sortKey: ASSET_SCHEMA.CURRENT_DEPT.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_DEPT.key] || '' },
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.PREV_USER.ui, align: 'center', render: a => a[ASSET_SCHEMA.PREV_USER.key] || '-' },
|
||||
{ header: '구매연월', sortKey: ASSET_SCHEMA.PURCHASE_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
|
||||
{ header: '시작일', align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
|
||||
{ header: '만료일', sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openSwModal } from '../../components/Modal/SWModal';
|
||||
import { sortAssets, formatInline } from '../../core/utils';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderSwList(container: HTMLElement) {
|
||||
const isInternal = state.activeSubTab === '내부SW';
|
||||
|
||||
createListView(container, {
|
||||
title: isInternal ? '내부SW' : '외부SW',
|
||||
dataSource: () => sortAssets(isInternal ? state.masterData.swInternal : state.masterData.swExternal),
|
||||
searchKeys: ['PRODUCT_NAME', 'CURRENT_USER', 'CURRENT_DEPT', 'ASSET_TYPE'],
|
||||
emptyMessage: '검색 결과가 없습니다.',
|
||||
filterOptions: {
|
||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.CURRENT_DEPT.ui})`,
|
||||
showField: true,
|
||||
showCorp: true,
|
||||
showDept: true,
|
||||
showType: true
|
||||
},
|
||||
onRowClick: (asset) => openSwModal(asset, 'view'),
|
||||
columns: isInternal ? [
|
||||
{ header: ASSET_SCHEMA.SW_FIELD.ui, sortKey: ASSET_SCHEMA.SW_FIELD.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_FIELD.key] || '' },
|
||||
{ header: ASSET_SCHEMA.DEV_OBJ.ui, sortKey: ASSET_SCHEMA.DEV_OBJ.key, align: 'center', render: a => a[ASSET_SCHEMA.DEV_OBJ.key] || '' },
|
||||
{ header: ASSET_SCHEMA.SW_STATUS.ui, sortKey: ASSET_SCHEMA.SW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_STATUS.key] || '보유중' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.SW_TYPE.ui, sortKey: ASSET_SCHEMA.SW_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_TYPE.key] || '내부' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
] : [
|
||||
{ header: '자산명', sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => a[ASSET_SCHEMA.PRODUCT_NAME.key] || '' },
|
||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.SW_STATUS.ui, sortKey: ASSET_SCHEMA.SW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_STATUS.key] || '사용중' },
|
||||
{ header: ASSET_SCHEMA.SW_FIELD.ui, sortKey: ASSET_SCHEMA.SW_FIELD.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_FIELD.key] || '' },
|
||||
{ header: ASSET_SCHEMA.CURRENT_DEPT.ui, sortKey: ASSET_SCHEMA.CURRENT_DEPT.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_DEPT.key] || '' },
|
||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||
{ header: ASSET_SCHEMA.PREV_USER.ui, align: 'center', render: a => a[ASSET_SCHEMA.PREV_USER.key] || '-' },
|
||||
{ header: '구매연월', sortKey: ASSET_SCHEMA.PURCHASE_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
|
||||
{ header: '시작일', align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
|
||||
{ header: '만료일', sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
|
||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openUserModal } from '../../components/Modal/UserModal';
|
||||
import { formatInline } from '../../core/utils';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderUserList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '사용자',
|
||||
dataSource: () => state.masterData.users || [],
|
||||
searchKeys: ['emp_no', 'user_name', 'dept_name', 'position', 'status'],
|
||||
filterOptions: {
|
||||
keywordLabel: '사번/이름/부서/직급 검색',
|
||||
showCorp: false,
|
||||
showDept: true,
|
||||
showType: false
|
||||
},
|
||||
onRowClick: (user) => openUserModal(user, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: '사번',
|
||||
sortKey: 'emp_no',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: u => formatInline(u.emp_no || '-')
|
||||
},
|
||||
{
|
||||
header: '이름',
|
||||
sortKey: 'user_name',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: u => formatInline(u.user_name || '-')
|
||||
},
|
||||
{
|
||||
header: '조직 (부서)',
|
||||
sortKey: 'dept_name',
|
||||
align: 'left',
|
||||
width: '25%',
|
||||
render: u => formatInline(u.dept_name || '-')
|
||||
},
|
||||
{
|
||||
header: '직급 (직무)',
|
||||
sortKey: 'position',
|
||||
align: 'left',
|
||||
width: '25%',
|
||||
render: u => formatInline(u.position || '-')
|
||||
},
|
||||
{
|
||||
header: '상태',
|
||||
sortKey: 'status',
|
||||
align: 'center',
|
||||
width: '10%',
|
||||
render: u => {
|
||||
const status = u.status || '재직';
|
||||
const badgeClass = status === '퇴직' ? 'badge-danger' : 'badge-success';
|
||||
return `<span class="badge ${badgeClass}">${status}</span>`;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
import { state } from '../../core/state';
|
||||
import { openUserModal } from '../../components/Modal/UserModal';
|
||||
import { formatInline } from '../../core/utils';
|
||||
import { createListView } from './ListFactory';
|
||||
|
||||
export function renderUserList(container: HTMLElement) {
|
||||
createListView(container, {
|
||||
title: '사용자',
|
||||
dataSource: () => state.masterData.users || [],
|
||||
searchKeys: ['emp_no', 'user_name', 'dept_name', 'position', 'status'],
|
||||
filterOptions: {
|
||||
keywordLabel: '사번/이름/부서/직급 검색',
|
||||
showCorp: false,
|
||||
showDept: true,
|
||||
showType: false
|
||||
},
|
||||
onRowClick: (user) => openUserModal(user, 'view'),
|
||||
columns: [
|
||||
{
|
||||
header: '사번',
|
||||
sortKey: 'emp_no',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: u => formatInline(u.emp_no || '-')
|
||||
},
|
||||
{
|
||||
header: '이름',
|
||||
sortKey: 'user_name',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
render: u => formatInline(u.user_name || '-')
|
||||
},
|
||||
{
|
||||
header: '조직 (부서)',
|
||||
sortKey: 'dept_name',
|
||||
align: 'left',
|
||||
width: '25%',
|
||||
render: u => formatInline(u.dept_name || '-')
|
||||
},
|
||||
{
|
||||
header: '직급 (직무)',
|
||||
sortKey: 'position',
|
||||
align: 'left',
|
||||
width: '25%',
|
||||
render: u => formatInline(u.position || '-')
|
||||
},
|
||||
{
|
||||
header: '상태',
|
||||
sortKey: 'status',
|
||||
align: 'center',
|
||||
width: '10%',
|
||||
render: u => {
|
||||
const status = u.status || '재직';
|
||||
const badgeClass = status === '퇴직' ? 'badge-danger' : 'badge-success';
|
||||
return `<span class="badge ${badgeClass}">${status}</span>`;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,150 +1,150 @@
|
||||
/* --- Page Header for Description --- */
|
||||
.page-header {
|
||||
padding: 1.5rem 2rem 0.5rem; /* Padding added for better whitespace */
|
||||
}
|
||||
|
||||
.page-title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--fs-lg);
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
font-size: var(--fs-base);
|
||||
color: var(--mute);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* --- Table View Styles --- */
|
||||
.table-container {
|
||||
flex: 1;
|
||||
background-color: var(--canvas);
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed; /* Force fixed layout to prevent horizontal scroll */
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.8rem 1rem;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; /* Show ... for long text */
|
||||
}
|
||||
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--canvas-soft) !important;
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 600;
|
||||
color: var(--mute);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.02em;
|
||||
box-shadow: inset 0 -1px 0 var(--hairline);
|
||||
text-align: center; /* Set default header alignment to center */
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: var(--fs-base);
|
||||
color: var(--primary);
|
||||
font-weight: 400;
|
||||
text-align: left; /* Set default data alignment to left */
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: var(--canvas-soft-2);
|
||||
}
|
||||
|
||||
/* 정렬 클래스 */
|
||||
.text-center { text-align: center !important; }
|
||||
.text-right { text-align: right !important; }
|
||||
.text-left { text-align: left !important; }
|
||||
|
||||
/* 메모 컬럼 전용 */
|
||||
.col-memo {
|
||||
white-space: nowrap !important; /* Keep as one line */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* --- Table Sorting --- */
|
||||
th.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
padding-right: 1.8rem !important;
|
||||
}
|
||||
|
||||
th.sortable:hover {
|
||||
background-color: var(--canvas-soft-2) !important;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
th.sortable::after {
|
||||
content: '↕';
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: var(--fs-xs);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
th.sortable.asc::after { content: '▲'; opacity: 1; color: var(--primary); }
|
||||
th.sortable.desc::after { content: '▼'; opacity: 1; color: var(--primary); }
|
||||
|
||||
/* --- Compact Table (Used in Dashboards/Modals) --- */
|
||||
.compact-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.compact-table th {
|
||||
padding: 0.75rem 0.5rem;
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 600;
|
||||
color: var(--mute);
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
background: var(--canvas-soft);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.compact-table td {
|
||||
padding: 0.75rem 0.5rem;
|
||||
font-size: var(--fs-sm);
|
||||
border-bottom: 1px solid var(--hairline-soft, #f5f5f5);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
|
||||
.compact-table tr.clickable-row:hover {
|
||||
background: var(--canvas-soft);
|
||||
cursor: pointer;
|
||||
}
|
||||
/* --- Page Header for Description --- */
|
||||
.page-header {
|
||||
padding: 1.5rem 2rem 0.5rem; /* Padding added for better whitespace */
|
||||
}
|
||||
|
||||
.page-title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--fs-lg);
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
font-size: var(--fs-base);
|
||||
color: var(--mute);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* --- Table View Styles --- */
|
||||
.table-container {
|
||||
flex: 1;
|
||||
background-color: var(--canvas);
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed; /* Force fixed layout to prevent horizontal scroll */
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.8rem 1rem;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; /* Show ... for long text */
|
||||
}
|
||||
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--canvas-soft) !important;
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 600;
|
||||
color: var(--mute);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.02em;
|
||||
box-shadow: inset 0 -1px 0 var(--hairline);
|
||||
text-align: center; /* Set default header alignment to center */
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: var(--fs-base);
|
||||
color: var(--primary);
|
||||
font-weight: 400;
|
||||
text-align: left; /* Set default data alignment to left */
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: var(--canvas-soft-2);
|
||||
}
|
||||
|
||||
/* 정렬 클래스 */
|
||||
.text-center { text-align: center !important; }
|
||||
.text-right { text-align: right !important; }
|
||||
.text-left { text-align: left !important; }
|
||||
|
||||
/* 메모 컬럼 전용 */
|
||||
.col-memo {
|
||||
white-space: nowrap !important; /* Keep as one line */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* --- Table Sorting --- */
|
||||
th.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
padding-right: 1.8rem !important;
|
||||
}
|
||||
|
||||
th.sortable:hover {
|
||||
background-color: var(--canvas-soft-2) !important;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
th.sortable::after {
|
||||
content: '↕';
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: var(--fs-xs);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
th.sortable.asc::after { content: '▲'; opacity: 1; color: var(--primary); }
|
||||
th.sortable.desc::after { content: '▼'; opacity: 1; color: var(--primary); }
|
||||
|
||||
/* --- Compact Table (Used in Dashboards/Modals) --- */
|
||||
.compact-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.compact-table th {
|
||||
padding: 0.75rem 0.5rem;
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 600;
|
||||
color: var(--mute);
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
background: var(--canvas-soft);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.compact-table td {
|
||||
padding: 0.75rem 0.5rem;
|
||||
font-size: var(--fs-sm);
|
||||
border-bottom: 1px solid var(--hairline-soft, #f5f5f5);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
|
||||
.compact-table tr.clickable-row:hover {
|
||||
background: var(--canvas-soft);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,291 +1,291 @@
|
||||
import { state } from '../core/state';
|
||||
import { openHwModal } from '../components/Modal/HWModal';
|
||||
import { ASSET_SCHEMA } from '../core/schema';
|
||||
import { LOCATION_DATA, IMAGE_LOCATIONS } from '../components/Modal/SharedData';
|
||||
|
||||
/**
|
||||
* 위치 중심 자산 현황 뷰 (Vercel Integrated)
|
||||
*/
|
||||
export async function renderLocationView(container: HTMLElement) {
|
||||
if (!container) return;
|
||||
|
||||
let currentLoc = '기술개발센터';
|
||||
let currentDetail = '서버실';
|
||||
let currentPage = 0;
|
||||
let mapConfig: any = {};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/maps');
|
||||
mapConfig = await res.json();
|
||||
} catch (err) { console.error('Failed to load map config', err); }
|
||||
|
||||
const render = () => {
|
||||
const locImages = (IMAGE_LOCATIONS[currentLoc] && IMAGE_LOCATIONS[currentLoc][currentDetail])
|
||||
? IMAGE_LOCATIONS[currentLoc][currentDetail]
|
||||
: [];
|
||||
const mapPath = locImages[currentPage] || '';
|
||||
|
||||
// 모든 하드웨어 카테고리에서 자산 검색
|
||||
const allHwAssets = [
|
||||
...state.masterData.pc,
|
||||
...state.masterData.server,
|
||||
...state.masterData.storage,
|
||||
...state.masterData.network,
|
||||
...state.masterData.equipment,
|
||||
...state.masterData.survey,
|
||||
...state.masterData.officeSupplies,
|
||||
...state.masterData.pcParts
|
||||
];
|
||||
|
||||
// map_config.json에 설정된 모든 박스를 복사해서 작업용으로 사용
|
||||
const tempBoxes = (mapConfig[mapPath] || []).map((b: any) => ({ ...b }));
|
||||
|
||||
// DB 데이터에서 현재 지도(mapPath) 및 위치와 좌표 정보(loc_x, loc_y)가 일치하는 자산 추출
|
||||
allHwAssets.forEach((asset: any) => {
|
||||
const photoPath = asset.location_photo || asset.loc_img || '';
|
||||
const hasCoords = asset.loc_x != null && asset.loc_y != null && asset.loc_x !== '' && asset.loc_y !== '' && asset.loc_x !== 'null' && asset.loc_y !== 'null';
|
||||
|
||||
if (hasCoords && photoPath.trim() === mapPath.trim()) {
|
||||
const ax = parseFloat(asset.loc_x);
|
||||
const ay = parseFloat(asset.loc_y);
|
||||
|
||||
// map_config.json에서 읽어온 박스들 중 x, y 좌표가 일치하는 빈 박스가 있는지 찾음 (오차범위 0.1 고려)
|
||||
const matchedBox = tempBoxes.find((b: any) => {
|
||||
const bx = parseFloat(b.x);
|
||||
const by = parseFloat(b.y);
|
||||
return Math.abs(bx - ax) < 0.1 && Math.abs(by - ay) < 0.1;
|
||||
});
|
||||
|
||||
if (matchedBox) {
|
||||
// 이미 매칭된 박스가 존재하고 asset_id가 비어있다면 해당 박스에 asset_id를 주입
|
||||
if (matchedBox.asset_id == null) {
|
||||
matchedBox.asset_id = asset.id;
|
||||
}
|
||||
} else {
|
||||
// 일치하는 기존 박스가 없을 때만 4x4 크기의 임시 박스로 동적 생성
|
||||
const alreadyMatched = tempBoxes.some((b: any) => b.asset_id === asset.id);
|
||||
if (!alreadyMatched) {
|
||||
tempBoxes.push({
|
||||
asset_id: asset.id,
|
||||
x: asset.loc_x,
|
||||
y: asset.loc_y,
|
||||
w: '4',
|
||||
h: '4',
|
||||
name: asset.asset_purpose || asset.asset_code || '미지정 자산'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 최종적으로 asset_id가 null이 아닌(자산이 정상 매핑되거나 갱신된) 박스들만 남겨서 렌더링
|
||||
const boxes = tempBoxes.filter((b: any) => b.asset_id != null);
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="location-view-wrapper">
|
||||
<!-- 상단 통합 바 (Unified Search Bar) -->
|
||||
<div class="location-filter-bar search-bar">
|
||||
<div class="search-item">
|
||||
<label class="list-view-toggle-label">
|
||||
<input type="checkbox" id="chk-list-view-loc" />
|
||||
목록보기
|
||||
</label>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<label>건물/위치</label>
|
||||
<select id="sel-loc-main">
|
||||
${Object.keys(LOCATION_DATA).map(loc => `<option value="${loc}" ${loc === currentLoc ? 'selected' : ''}>${loc}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<label>상세 위치</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<select id="sel-loc-detail">
|
||||
${(LOCATION_DATA[currentLoc] || []).map(det => `<option value="${det}" ${det === currentDetail ? 'selected' : ''}>${det}</option>`).join('')}
|
||||
</select>
|
||||
|
||||
<!-- 페이지네이션 -->
|
||||
${locImages.length > 1 ? `
|
||||
<div class="map-pagination-group">
|
||||
<div class="page-btns flex gap-1">
|
||||
<button id="btn-prev-page" class="btn btn-outline btn-sm" ${currentPage === 0 ? 'disabled' : ''}>이전</button>
|
||||
<button id="btn-next-page" class="btn btn-outline btn-sm" ${currentPage === locImages.length - 1 ? 'disabled' : ''}>다음</button>
|
||||
</div>
|
||||
<span class="page-info">(${currentPage + 1} / ${locImages.length})</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="location-main-content">
|
||||
<!-- 지도 섹션 -->
|
||||
<div class="map-container-section">
|
||||
<div class="map-frame-wrapper">
|
||||
${mapPath ? `
|
||||
<img src="${mapPath}" id="main-map-img" class="map-image">
|
||||
<div id="box-overlay" class="map-overlay">
|
||||
${boxes.map((box: any, idx: number) => {
|
||||
const asset = allHwAssets.find(a => a.id === box.asset_id);
|
||||
const name = asset ? ((asset as any).asset_purpose || asset.asset_code) : (box.name || `#${idx+1}`);
|
||||
// w, h가 없거나 너무 작으면 최소 크기(3%) 보장하여 영역으로 표시
|
||||
const width = Math.max(parseFloat(box.w || '3'), 3);
|
||||
const height = Math.max(parseFloat(box.h || '3'), 3);
|
||||
return `
|
||||
<div class="location-box-area"
|
||||
data-asset-id="${box.asset_id}"
|
||||
data-name="${name}"
|
||||
style="left:${box.x}%; top:${box.y}%; width:${width}%; height:${height}%;
|
||||
border: 2px solid var(--primary-color); background: rgba(30, 81, 73, 0.1); cursor:pointer; pointer-events: auto; position: absolute;">
|
||||
</div>
|
||||
`}).join('')}
|
||||
</div>
|
||||
` : '<div class="no-map-message">해당 위치의 도면이 등록되지 않았습니다.</div>'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 상세 정보 섹션 -->
|
||||
<div class="asset-list-section">
|
||||
<div class="section-header">
|
||||
<h4 id="loc-list-title" class="sidebar-title">구역을 선택하세요</h4>
|
||||
</div>
|
||||
<div id="loc-asset-table-container" class="mini-table-wrapper">
|
||||
<div class="empty-state">지도에서 자산 위치를 클릭하세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const syncOverlaySize = () => {
|
||||
const img = container.querySelector('#main-map-img') as HTMLImageElement;
|
||||
const overlay = container.querySelector('#box-overlay') as HTMLElement;
|
||||
|
||||
if (img && overlay && img.complete) {
|
||||
overlay.style.width = img.clientWidth + 'px';
|
||||
overlay.style.height = img.clientHeight + 'px';
|
||||
overlay.style.left = img.offsetLeft + 'px';
|
||||
overlay.style.top = img.offsetTop + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
const img = container.querySelector('#main-map-img') as HTMLImageElement;
|
||||
if (img) {
|
||||
if (img.complete) {
|
||||
syncOverlaySize();
|
||||
setTimeout(syncOverlaySize, 50);
|
||||
} else {
|
||||
img.onload = syncOverlaySize;
|
||||
}
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', syncOverlaySize);
|
||||
window.addEventListener('resize', syncOverlaySize);
|
||||
|
||||
const selMain = container.querySelector('#sel-loc-main') as HTMLSelectElement;
|
||||
selMain?.addEventListener('change', () => {
|
||||
currentLoc = selMain.value;
|
||||
currentDetail = LOCATION_DATA[currentLoc][0];
|
||||
currentPage = 0;
|
||||
render();
|
||||
});
|
||||
|
||||
const selDetail = container.querySelector('#sel-loc-detail') as HTMLSelectElement;
|
||||
selDetail?.addEventListener('change', () => {
|
||||
currentDetail = selDetail.value;
|
||||
currentPage = 0;
|
||||
render();
|
||||
});
|
||||
|
||||
container.querySelector('#btn-prev-page')?.addEventListener('click', () => { currentPage--; render(); });
|
||||
container.querySelector('#btn-next-page')?.addEventListener('click', () => { currentPage++; render(); });
|
||||
|
||||
const chkBox = container.querySelector('#chk-list-view-loc') as HTMLInputElement;
|
||||
|
||||
if (chkBox) {
|
||||
chkBox.checked = state.viewMode === 'list';
|
||||
const handleToggle = () => {
|
||||
const isListMode = chkBox.checked;
|
||||
if (isListMode) {
|
||||
state.viewMode = 'list';
|
||||
} else {
|
||||
state.viewMode = 'location';
|
||||
}
|
||||
window.dispatchEvent(new Event('refresh-view'));
|
||||
};
|
||||
chkBox.addEventListener('change', handleToggle);
|
||||
}
|
||||
|
||||
container.querySelectorAll('.location-box-area').forEach(box => {
|
||||
box.addEventListener('click', () => {
|
||||
const assetId = box.getAttribute('data-asset-id');
|
||||
if (!assetId) return;
|
||||
|
||||
const targetAsset = allHwAssets.find(a => a.id === assetId);
|
||||
|
||||
if (targetAsset) renderAssetDetail(targetAsset);
|
||||
container.querySelectorAll('.location-box-area').forEach(b => (b as HTMLElement).style.background = 'rgba(30, 81, 73, 0.1)');
|
||||
(box as HTMLElement).style.background = 'rgba(30, 81, 73, 0.4)';
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const renderAssetDetail = (asset: any) => {
|
||||
const title = container.querySelector('#loc-list-title')!;
|
||||
const tableContainer = container.querySelector('#loc-asset-table-container')!;
|
||||
|
||||
title.innerHTML = `
|
||||
<div class="detail-header-actions">
|
||||
<div class="header-identity">
|
||||
<span class="asset-code-title">${asset.asset_code || '미부여'}</span>
|
||||
<span class="service-type-badge">${asset.service_type || '운영'}</span>
|
||||
<span class="asset-type-label">${asset.asset_type || 'PC'}</span>
|
||||
</div>
|
||||
<button id="btn-view-from-loc" class="btn btn-primary btn-sm">조회</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const fields = [
|
||||
{ label: ASSET_SCHEMA.CURRENT_DEPT.ui, value: asset.current_dept },
|
||||
{ label: ASSET_SCHEMA.HW_STATUS.ui, value: asset.hw_status },
|
||||
{ label: ASSET_SCHEMA.MANAGER_MAIN.ui, value: asset.manager_primary },
|
||||
{ label: ASSET_SCHEMA.MANAGER_SUB.ui, value: asset.manager_secondary },
|
||||
{ label: ASSET_SCHEMA.ASSET_PURPOSE.ui, value: asset.asset_purpose, fullWidth: true },
|
||||
{ label: ASSET_SCHEMA.MODEL_NAME.ui, value: asset.model_name },
|
||||
{ label: ASSET_SCHEMA.OS.ui, value: asset.os },
|
||||
{ label: ASSET_SCHEMA.CPU.ui, value: asset.cpu },
|
||||
{ label: ASSET_SCHEMA.RAM.ui, value: asset.ram },
|
||||
{ label: ASSET_SCHEMA.GPU.ui, value: asset.gpu, fullWidth: true },
|
||||
{ label: ASSET_SCHEMA.IP_ADDR.ui, value: asset.ip_address },
|
||||
{ label: ASSET_SCHEMA.MAC_ADDR.ui, value: asset.mac_address },
|
||||
{ label: ASSET_SCHEMA.REMOTE_TOOL.ui, value: asset.remote_tool },
|
||||
{ label: ASSET_SCHEMA.MONITORING.ui, value: asset.monitoring },
|
||||
{ label: ASSET_SCHEMA.MEMO.ui, value: asset.memo, fullWidth: true }
|
||||
];
|
||||
|
||||
const sectionsHTML = `
|
||||
<div class="detail-section" style="margin-bottom: 0;">
|
||||
<div class="detail-grid-2col" style="gap: 0.75rem 1rem;">
|
||||
${fields.map(f => `
|
||||
<div class="detail-item ${f.fullWidth ? 'full-width' : ''}">
|
||||
<div class="detail-label-sm">${f.label}</div>
|
||||
<div class="detail-value-lg">${f.value || '-'}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
tableContainer.innerHTML = `
|
||||
<div class="asset-detail-sidebar">
|
||||
${sectionsHTML}
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.querySelector('#btn-view-from-loc')?.addEventListener('click', () => {
|
||||
openHwModal(asset, 'view');
|
||||
});
|
||||
};
|
||||
|
||||
render();
|
||||
}
|
||||
import { state } from '../core/state';
|
||||
import { openHwModal } from '../components/Modal/HWModal';
|
||||
import { ASSET_SCHEMA } from '../core/schema';
|
||||
import { LOCATION_DATA, IMAGE_LOCATIONS } from '../components/Modal/SharedData';
|
||||
|
||||
/**
|
||||
* 위치 중심 자산 현황 뷰 (Vercel Integrated)
|
||||
*/
|
||||
export async function renderLocationView(container: HTMLElement) {
|
||||
if (!container) return;
|
||||
|
||||
let currentLoc = '기술개발센터';
|
||||
let currentDetail = '서버실';
|
||||
let currentPage = 0;
|
||||
let mapConfig: any = {};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/maps');
|
||||
mapConfig = await res.json();
|
||||
} catch (err) { console.error('Failed to load map config', err); }
|
||||
|
||||
const render = () => {
|
||||
const locImages = (IMAGE_LOCATIONS[currentLoc] && IMAGE_LOCATIONS[currentLoc][currentDetail])
|
||||
? IMAGE_LOCATIONS[currentLoc][currentDetail]
|
||||
: [];
|
||||
const mapPath = locImages[currentPage] || '';
|
||||
|
||||
// 모든 하드웨어 카테고리에서 자산 검색
|
||||
const allHwAssets = [
|
||||
...state.masterData.pc,
|
||||
...state.masterData.server,
|
||||
...state.masterData.storage,
|
||||
...state.masterData.network,
|
||||
...state.masterData.equipment,
|
||||
...state.masterData.survey,
|
||||
...state.masterData.officeSupplies,
|
||||
...state.masterData.pcParts
|
||||
];
|
||||
|
||||
// map_config.json에 설정된 모든 박스를 복사해서 작업용으로 사용
|
||||
const tempBoxes = (mapConfig[mapPath] || []).map((b: any) => ({ ...b }));
|
||||
|
||||
// DB 데이터에서 현재 지도(mapPath) 및 위치와 좌표 정보(loc_x, loc_y)가 일치하는 자산 추출
|
||||
allHwAssets.forEach((asset: any) => {
|
||||
const photoPath = asset.location_photo || asset.loc_img || '';
|
||||
const hasCoords = asset.loc_x != null && asset.loc_y != null && asset.loc_x !== '' && asset.loc_y !== '' && asset.loc_x !== 'null' && asset.loc_y !== 'null';
|
||||
|
||||
if (hasCoords && photoPath.trim() === mapPath.trim()) {
|
||||
const ax = parseFloat(asset.loc_x);
|
||||
const ay = parseFloat(asset.loc_y);
|
||||
|
||||
// map_config.json에서 읽어온 박스들 중 x, y 좌표가 일치하는 빈 박스가 있는지 찾음 (오차범위 0.1 고려)
|
||||
const matchedBox = tempBoxes.find((b: any) => {
|
||||
const bx = parseFloat(b.x);
|
||||
const by = parseFloat(b.y);
|
||||
return Math.abs(bx - ax) < 0.1 && Math.abs(by - ay) < 0.1;
|
||||
});
|
||||
|
||||
if (matchedBox) {
|
||||
// 이미 매칭된 박스가 존재하고 asset_id가 비어있다면 해당 박스에 asset_id를 주입
|
||||
if (matchedBox.asset_id == null) {
|
||||
matchedBox.asset_id = asset.id;
|
||||
}
|
||||
} else {
|
||||
// 일치하는 기존 박스가 없을 때만 4x4 크기의 임시 박스로 동적 생성
|
||||
const alreadyMatched = tempBoxes.some((b: any) => b.asset_id === asset.id);
|
||||
if (!alreadyMatched) {
|
||||
tempBoxes.push({
|
||||
asset_id: asset.id,
|
||||
x: asset.loc_x,
|
||||
y: asset.loc_y,
|
||||
w: '4',
|
||||
h: '4',
|
||||
name: asset.asset_purpose || asset.asset_code || '미지정 자산'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 최종적으로 asset_id가 null이 아닌(자산이 정상 매핑되거나 갱신된) 박스들만 남겨서 렌더링
|
||||
const boxes = tempBoxes.filter((b: any) => b.asset_id != null);
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="location-view-wrapper">
|
||||
<!-- 상단 통합 바 (Unified Search Bar) -->
|
||||
<div class="location-filter-bar search-bar">
|
||||
<div class="search-item">
|
||||
<label class="list-view-toggle-label">
|
||||
<input type="checkbox" id="chk-list-view-loc" />
|
||||
목록보기
|
||||
</label>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<label>건물/위치</label>
|
||||
<select id="sel-loc-main">
|
||||
${Object.keys(LOCATION_DATA).map(loc => `<option value="${loc}" ${loc === currentLoc ? 'selected' : ''}>${loc}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<label>상세 위치</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<select id="sel-loc-detail">
|
||||
${(LOCATION_DATA[currentLoc] || []).map(det => `<option value="${det}" ${det === currentDetail ? 'selected' : ''}>${det}</option>`).join('')}
|
||||
</select>
|
||||
|
||||
<!-- 페이지네이션 -->
|
||||
${locImages.length > 1 ? `
|
||||
<div class="map-pagination-group">
|
||||
<div class="page-btns flex gap-1">
|
||||
<button id="btn-prev-page" class="btn btn-outline btn-sm" ${currentPage === 0 ? 'disabled' : ''}>이전</button>
|
||||
<button id="btn-next-page" class="btn btn-outline btn-sm" ${currentPage === locImages.length - 1 ? 'disabled' : ''}>다음</button>
|
||||
</div>
|
||||
<span class="page-info">(${currentPage + 1} / ${locImages.length})</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="location-main-content">
|
||||
<!-- 지도 섹션 -->
|
||||
<div class="map-container-section">
|
||||
<div class="map-frame-wrapper">
|
||||
${mapPath ? `
|
||||
<img src="${mapPath}" id="main-map-img" class="map-image">
|
||||
<div id="box-overlay" class="map-overlay">
|
||||
${boxes.map((box: any, idx: number) => {
|
||||
const asset = allHwAssets.find(a => a.id === box.asset_id);
|
||||
const name = asset ? ((asset as any).asset_purpose || asset.asset_code) : (box.name || `#${idx+1}`);
|
||||
// w, h가 없거나 너무 작으면 최소 크기(3%) 보장하여 영역으로 표시
|
||||
const width = Math.max(parseFloat(box.w || '3'), 3);
|
||||
const height = Math.max(parseFloat(box.h || '3'), 3);
|
||||
return `
|
||||
<div class="location-box-area"
|
||||
data-asset-id="${box.asset_id}"
|
||||
data-name="${name}"
|
||||
style="left:${box.x}%; top:${box.y}%; width:${width}%; height:${height}%;
|
||||
border: 2px solid var(--primary-color); background: rgba(30, 81, 73, 0.1); cursor:pointer; pointer-events: auto; position: absolute;">
|
||||
</div>
|
||||
`}).join('')}
|
||||
</div>
|
||||
` : '<div class="no-map-message">해당 위치의 도면이 등록되지 않았습니다.</div>'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 상세 정보 섹션 -->
|
||||
<div class="asset-list-section">
|
||||
<div class="section-header">
|
||||
<h4 id="loc-list-title" class="sidebar-title">구역을 선택하세요</h4>
|
||||
</div>
|
||||
<div id="loc-asset-table-container" class="mini-table-wrapper">
|
||||
<div class="empty-state">지도에서 자산 위치를 클릭하세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const syncOverlaySize = () => {
|
||||
const img = container.querySelector('#main-map-img') as HTMLImageElement;
|
||||
const overlay = container.querySelector('#box-overlay') as HTMLElement;
|
||||
|
||||
if (img && overlay && img.complete) {
|
||||
overlay.style.width = img.clientWidth + 'px';
|
||||
overlay.style.height = img.clientHeight + 'px';
|
||||
overlay.style.left = img.offsetLeft + 'px';
|
||||
overlay.style.top = img.offsetTop + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
const img = container.querySelector('#main-map-img') as HTMLImageElement;
|
||||
if (img) {
|
||||
if (img.complete) {
|
||||
syncOverlaySize();
|
||||
setTimeout(syncOverlaySize, 50);
|
||||
} else {
|
||||
img.onload = syncOverlaySize;
|
||||
}
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', syncOverlaySize);
|
||||
window.addEventListener('resize', syncOverlaySize);
|
||||
|
||||
const selMain = container.querySelector('#sel-loc-main') as HTMLSelectElement;
|
||||
selMain?.addEventListener('change', () => {
|
||||
currentLoc = selMain.value;
|
||||
currentDetail = LOCATION_DATA[currentLoc][0];
|
||||
currentPage = 0;
|
||||
render();
|
||||
});
|
||||
|
||||
const selDetail = container.querySelector('#sel-loc-detail') as HTMLSelectElement;
|
||||
selDetail?.addEventListener('change', () => {
|
||||
currentDetail = selDetail.value;
|
||||
currentPage = 0;
|
||||
render();
|
||||
});
|
||||
|
||||
container.querySelector('#btn-prev-page')?.addEventListener('click', () => { currentPage--; render(); });
|
||||
container.querySelector('#btn-next-page')?.addEventListener('click', () => { currentPage++; render(); });
|
||||
|
||||
const chkBox = container.querySelector('#chk-list-view-loc') as HTMLInputElement;
|
||||
|
||||
if (chkBox) {
|
||||
chkBox.checked = state.viewMode === 'list';
|
||||
const handleToggle = () => {
|
||||
const isListMode = chkBox.checked;
|
||||
if (isListMode) {
|
||||
state.viewMode = 'list';
|
||||
} else {
|
||||
state.viewMode = 'location';
|
||||
}
|
||||
window.dispatchEvent(new Event('refresh-view'));
|
||||
};
|
||||
chkBox.addEventListener('change', handleToggle);
|
||||
}
|
||||
|
||||
container.querySelectorAll('.location-box-area').forEach(box => {
|
||||
box.addEventListener('click', () => {
|
||||
const assetId = box.getAttribute('data-asset-id');
|
||||
if (!assetId) return;
|
||||
|
||||
const targetAsset = allHwAssets.find(a => a.id === assetId);
|
||||
|
||||
if (targetAsset) renderAssetDetail(targetAsset);
|
||||
container.querySelectorAll('.location-box-area').forEach(b => (b as HTMLElement).style.background = 'rgba(30, 81, 73, 0.1)');
|
||||
(box as HTMLElement).style.background = 'rgba(30, 81, 73, 0.4)';
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const renderAssetDetail = (asset: any) => {
|
||||
const title = container.querySelector('#loc-list-title')!;
|
||||
const tableContainer = container.querySelector('#loc-asset-table-container')!;
|
||||
|
||||
title.innerHTML = `
|
||||
<div class="detail-header-actions">
|
||||
<div class="header-identity">
|
||||
<span class="asset-code-title">${asset.asset_code || '미부여'}</span>
|
||||
<span class="service-type-badge">${asset.service_type || '운영'}</span>
|
||||
<span class="asset-type-label">${asset.asset_type || 'PC'}</span>
|
||||
</div>
|
||||
<button id="btn-view-from-loc" class="btn btn-primary btn-sm">조회</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const fields = [
|
||||
{ label: ASSET_SCHEMA.CURRENT_DEPT.ui, value: asset.current_dept },
|
||||
{ label: ASSET_SCHEMA.HW_STATUS.ui, value: asset.hw_status },
|
||||
{ label: ASSET_SCHEMA.MANAGER_MAIN.ui, value: asset.manager_primary },
|
||||
{ label: ASSET_SCHEMA.MANAGER_SUB.ui, value: asset.manager_secondary },
|
||||
{ label: ASSET_SCHEMA.ASSET_PURPOSE.ui, value: asset.asset_purpose, fullWidth: true },
|
||||
{ label: ASSET_SCHEMA.MODEL_NAME.ui, value: asset.model_name },
|
||||
{ label: ASSET_SCHEMA.OS.ui, value: asset.os },
|
||||
{ label: ASSET_SCHEMA.CPU.ui, value: asset.cpu },
|
||||
{ label: ASSET_SCHEMA.RAM.ui, value: asset.ram },
|
||||
{ label: ASSET_SCHEMA.GPU.ui, value: asset.gpu, fullWidth: true },
|
||||
{ label: ASSET_SCHEMA.IP_ADDR.ui, value: asset.ip_address },
|
||||
{ label: ASSET_SCHEMA.MAC_ADDR.ui, value: asset.mac_address },
|
||||
{ label: ASSET_SCHEMA.REMOTE_TOOL.ui, value: asset.remote_tool },
|
||||
{ label: ASSET_SCHEMA.MONITORING.ui, value: asset.monitoring },
|
||||
{ label: ASSET_SCHEMA.MEMO.ui, value: asset.memo, fullWidth: true }
|
||||
];
|
||||
|
||||
const sectionsHTML = `
|
||||
<div class="detail-section" style="margin-bottom: 0;">
|
||||
<div class="detail-grid-2col" style="gap: 0.75rem 1rem;">
|
||||
${fields.map(f => `
|
||||
<div class="detail-item ${f.fullWidth ? 'full-width' : ''}">
|
||||
<div class="detail-label-sm">${f.label}</div>
|
||||
<div class="detail-value-lg">${f.value || '-'}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
tableContainer.innerHTML = `
|
||||
<div class="asset-detail-sidebar">
|
||||
${sectionsHTML}
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.querySelector('#btn-view-from-loc')?.addEventListener('click', () => {
|
||||
openHwModal(asset, 'view');
|
||||
});
|
||||
};
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -1,299 +1,299 @@
|
||||
import { IMAGE_LOCATIONS } from '../components/Modal/SharedData';
|
||||
import { API_BASE_URL } from '../core/utils';
|
||||
import { createIcons, X, Save, Trash2, ChevronLeft, ChevronRight } from 'lucide';
|
||||
|
||||
export class MapEditor {
|
||||
private container: HTMLElement;
|
||||
private wrapper: HTMLElement;
|
||||
private img: HTMLImageElement;
|
||||
private boxListEl: HTMLElement;
|
||||
private pathLabel: HTMLElement;
|
||||
private statusEl: HTMLElement;
|
||||
private saveBtn: HTMLButtonElement;
|
||||
private fileSidebar: HTMLElement;
|
||||
|
||||
private allMapConfig: Record<string, any[]> = {};
|
||||
private boxes: any[] = [];
|
||||
private isDrawing: boolean = false;
|
||||
private startX: number = 0;
|
||||
private startY: number = 0;
|
||||
private currentBox: HTMLElement | null = null;
|
||||
private currentPath: string = '';
|
||||
private assetOptions: {id: string, name: string}[] = [];
|
||||
|
||||
constructor() {
|
||||
this.container = document.getElementById('container')!;
|
||||
this.wrapper = document.getElementById('wrapper')!;
|
||||
this.img = document.getElementById('target-img') as HTMLImageElement;
|
||||
this.boxListEl = document.getElementById('box-list')!;
|
||||
this.pathLabel = document.getElementById('current-path')!;
|
||||
this.statusEl = document.getElementById('save-status')!;
|
||||
this.saveBtn = document.getElementById('btn-save-server') as HTMLButtonElement;
|
||||
this.fileSidebar = document.getElementById('file-sidebar')!;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.renderFileSidebar();
|
||||
await this.loadConfig();
|
||||
await this.loadAssets();
|
||||
this.bindEvents();
|
||||
this.selectFirstFile();
|
||||
createIcons({ icons: { X, Save, Trash2, ChevronLeft, ChevronRight } });
|
||||
}
|
||||
|
||||
private async loadAssets() {
|
||||
try {
|
||||
const res = await fetch(`http://${location.hostname}:3000/api/assets/master`);
|
||||
const masterData = await res.json();
|
||||
const allHw = [
|
||||
...(masterData.pc || []),
|
||||
...(masterData.server || []),
|
||||
...(masterData.storage || []),
|
||||
...(masterData.network || []),
|
||||
...(masterData.equipment || []),
|
||||
...(masterData.survey || []),
|
||||
...(masterData.officeSupplies || []),
|
||||
...(masterData.pcParts || [])
|
||||
];
|
||||
this.assetOptions = allHw.map(a => ({
|
||||
id: a.id,
|
||||
name: `[${a.asset_code}] ${a.asset_purpose || a.model_name || a.category}`
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Failed to load assets for mapping', err);
|
||||
}
|
||||
}
|
||||
|
||||
private renderFileSidebar() {
|
||||
let html = '';
|
||||
Object.entries(IMAGE_LOCATIONS).forEach(([bldg, details]) => {
|
||||
html += `<div class="folder-item">${bldg}</div>`;
|
||||
Object.entries(details).forEach(([detail, paths]) => {
|
||||
paths.forEach(path => {
|
||||
const fileName = path.split('/').pop() || path;
|
||||
html += `<div class="file-item" data-path="${path}">${fileName}</div>`;
|
||||
});
|
||||
});
|
||||
});
|
||||
this.fileSidebar.innerHTML = html;
|
||||
|
||||
this.fileSidebar.querySelectorAll('.file-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
this.fileSidebar.querySelectorAll('.file-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
this.renderCurrentFile();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private selectFirstFile() {
|
||||
const firstItem = this.fileSidebar.querySelector('.file-item') as HTMLElement;
|
||||
if (firstItem) {
|
||||
firstItem.classList.add('active');
|
||||
this.renderCurrentFile();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/maps`);
|
||||
this.allMapConfig = await res.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to load config:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private renderCurrentFile() {
|
||||
const activeItem = this.fileSidebar.querySelector('.file-item.active') as HTMLElement;
|
||||
if (!activeItem) return;
|
||||
|
||||
this.currentPath = activeItem.dataset.path || '';
|
||||
this.boxes = this.allMapConfig[this.currentPath] || [];
|
||||
this.pathLabel.textContent = this.currentPath;
|
||||
this.img.src = this.currentPath;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
this.wrapper.addEventListener('mousedown', (e) => {
|
||||
if (e.button !== 0) return;
|
||||
this.isDrawing = true;
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
this.startX = e.clientX - rect.left;
|
||||
this.startY = e.clientY - rect.top;
|
||||
|
||||
this.currentBox = document.createElement('div');
|
||||
this.currentBox.className = 'draw-box';
|
||||
this.currentBox.style.left = this.startX + 'px';
|
||||
this.currentBox.style.top = this.startY + 'px';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'box-label';
|
||||
label.textContent = '#' + (this.boxes.length + 1);
|
||||
this.currentBox.appendChild(label);
|
||||
|
||||
this.wrapper.appendChild(this.currentBox);
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
if (!this.isDrawing || !this.currentBox) return;
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const currentX = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
||||
const currentY = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
|
||||
|
||||
const width = currentX - this.startX;
|
||||
const height = currentY - this.startY;
|
||||
|
||||
this.currentBox.style.width = Math.abs(width) + 'px';
|
||||
this.currentBox.style.height = Math.abs(height) + 'px';
|
||||
this.currentBox.style.left = (width > 0 ? this.startX : currentX) + 'px';
|
||||
this.currentBox.style.top = (height > 0 ? this.startY : currentY) + 'px';
|
||||
});
|
||||
|
||||
window.addEventListener('mouseup', () => {
|
||||
if (!this.isDrawing || !this.currentBox) return;
|
||||
this.isDrawing = false;
|
||||
|
||||
const width = parseFloat(this.currentBox.style.width);
|
||||
const height = parseFloat(this.currentBox.style.height);
|
||||
|
||||
if (width > 3 && height > 3) {
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const boxData = {
|
||||
x: (parseFloat(this.currentBox.style.left) / rect.width * 100).toFixed(2),
|
||||
y: (parseFloat(this.currentBox.style.top) / rect.height * 100).toFixed(2),
|
||||
w: (width / rect.width * 100).toFixed(2),
|
||||
h: (height / rect.height * 100).toFixed(2),
|
||||
asset_id: null
|
||||
};
|
||||
this.boxes.push(boxData);
|
||||
this.render();
|
||||
}
|
||||
|
||||
this.currentBox.remove();
|
||||
this.currentBox = null;
|
||||
});
|
||||
|
||||
(window as any).removeBox = (index: number) => {
|
||||
this.boxes.splice(index, 1);
|
||||
this.render();
|
||||
};
|
||||
|
||||
document.getElementById('btn-clear-all')?.addEventListener('click', () => {
|
||||
if(confirm('모든 박스를 삭제할까요?')) {
|
||||
this.boxes = [];
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btn-save-server')?.addEventListener('click', () => this.saveToServer());
|
||||
}
|
||||
|
||||
private async saveToServer() {
|
||||
if (!this.currentPath) return;
|
||||
|
||||
try {
|
||||
this.saveBtn.disabled = true;
|
||||
this.saveBtn.textContent = '저장 중...';
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/maps/save`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: this.currentPath, boxes: this.boxes })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.allMapConfig[this.currentPath] = [...this.boxes];
|
||||
this.statusEl.textContent = '✅ 서버 저장 완료 (' + new Date().toLocaleTimeString() + ')';
|
||||
setTimeout(() => this.statusEl.textContent = '', 3000);
|
||||
} else {
|
||||
alert('저장 실패!');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('서버 연결 오류!');
|
||||
} finally {
|
||||
this.saveBtn.disabled = false;
|
||||
this.saveBtn.textContent = '서버에 즉시 저장';
|
||||
}
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.boxListEl.innerHTML = '';
|
||||
const oldBoxes = this.wrapper.querySelectorAll('.placed-box');
|
||||
oldBoxes.forEach(b => b.remove());
|
||||
|
||||
this.boxes.forEach((box, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'placed-box';
|
||||
div.style.left = box.x + '%';
|
||||
div.style.top = box.y + '%';
|
||||
div.style.width = box.w + '%';
|
||||
div.style.height = box.h + '%';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'box-label';
|
||||
label.textContent = '#' + (i + 1);
|
||||
div.appendChild(label);
|
||||
|
||||
this.wrapper.appendChild(div);
|
||||
|
||||
// Create asset options dropdown
|
||||
let optionsHtml = '<option value="">-- 자산 매핑 안 됨 --</option>';
|
||||
this.assetOptions.forEach(opt => {
|
||||
const selected = box.asset_id === opt.id ? 'selected' : '';
|
||||
optionsHtml += `<option value="${opt.id}" ${selected}>${opt.name}</option>`;
|
||||
});
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'box-item';
|
||||
item.innerHTML = `
|
||||
<div class="box-header">
|
||||
<span class="box-index">#${i+1}</span>
|
||||
<button class="btn-del" onclick="removeBox(${i})">×</button>
|
||||
</div>
|
||||
<div class="box-inputs margin-bottom">
|
||||
<select data-index="${i}" data-prop="asset_id">
|
||||
${optionsHtml}
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-inputs">
|
||||
<div class="input-group">
|
||||
<label>X</label>
|
||||
<input type="number" step="0.01" value="${box.x}" data-index="${i}" data-prop="x">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>Y</label>
|
||||
<input type="number" step="0.01" value="${box.y}" data-index="${i}" data-prop="y">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>W</label>
|
||||
<input type="number" step="0.01" value="${box.w}" data-index="${i}" data-prop="w">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>H</label>
|
||||
<input type="number" step="0.01" value="${box.h}" data-index="${i}" data-prop="h">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.boxListEl.appendChild(item);
|
||||
});
|
||||
|
||||
// Add events to new inputs and selects
|
||||
this.boxListEl.querySelectorAll('input, select').forEach(input => {
|
||||
input.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement | HTMLSelectElement;
|
||||
const index = parseInt(target.dataset.index!);
|
||||
const prop = target.dataset.prop!;
|
||||
|
||||
if (this.boxes[index]) {
|
||||
if (prop === 'asset_id') {
|
||||
this.boxes[index][prop] = target.value || null;
|
||||
} else {
|
||||
this.boxes[index][prop] = parseFloat(target.value).toFixed(2);
|
||||
this.render(); // Re-render to update the map visual size
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
import { IMAGE_LOCATIONS } from '../components/Modal/SharedData';
|
||||
import { API_BASE_URL } from '../core/utils';
|
||||
import { createIcons, X, Save, Trash2, ChevronLeft, ChevronRight } from 'lucide';
|
||||
|
||||
export class MapEditor {
|
||||
private container: HTMLElement;
|
||||
private wrapper: HTMLElement;
|
||||
private img: HTMLImageElement;
|
||||
private boxListEl: HTMLElement;
|
||||
private pathLabel: HTMLElement;
|
||||
private statusEl: HTMLElement;
|
||||
private saveBtn: HTMLButtonElement;
|
||||
private fileSidebar: HTMLElement;
|
||||
|
||||
private allMapConfig: Record<string, any[]> = {};
|
||||
private boxes: any[] = [];
|
||||
private isDrawing: boolean = false;
|
||||
private startX: number = 0;
|
||||
private startY: number = 0;
|
||||
private currentBox: HTMLElement | null = null;
|
||||
private currentPath: string = '';
|
||||
private assetOptions: {id: string, name: string}[] = [];
|
||||
|
||||
constructor() {
|
||||
this.container = document.getElementById('container')!;
|
||||
this.wrapper = document.getElementById('wrapper')!;
|
||||
this.img = document.getElementById('target-img') as HTMLImageElement;
|
||||
this.boxListEl = document.getElementById('box-list')!;
|
||||
this.pathLabel = document.getElementById('current-path')!;
|
||||
this.statusEl = document.getElementById('save-status')!;
|
||||
this.saveBtn = document.getElementById('btn-save-server') as HTMLButtonElement;
|
||||
this.fileSidebar = document.getElementById('file-sidebar')!;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.renderFileSidebar();
|
||||
await this.loadConfig();
|
||||
await this.loadAssets();
|
||||
this.bindEvents();
|
||||
this.selectFirstFile();
|
||||
createIcons({ icons: { X, Save, Trash2, ChevronLeft, ChevronRight } });
|
||||
}
|
||||
|
||||
private async loadAssets() {
|
||||
try {
|
||||
const res = await fetch(`http://${location.hostname}:3000/api/assets/master`);
|
||||
const masterData = await res.json();
|
||||
const allHw = [
|
||||
...(masterData.pc || []),
|
||||
...(masterData.server || []),
|
||||
...(masterData.storage || []),
|
||||
...(masterData.network || []),
|
||||
...(masterData.equipment || []),
|
||||
...(masterData.survey || []),
|
||||
...(masterData.officeSupplies || []),
|
||||
...(masterData.pcParts || [])
|
||||
];
|
||||
this.assetOptions = allHw.map(a => ({
|
||||
id: a.id,
|
||||
name: `[${a.asset_code}] ${a.asset_purpose || a.model_name || a.category}`
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Failed to load assets for mapping', err);
|
||||
}
|
||||
}
|
||||
|
||||
private renderFileSidebar() {
|
||||
let html = '';
|
||||
Object.entries(IMAGE_LOCATIONS).forEach(([bldg, details]) => {
|
||||
html += `<div class="folder-item">${bldg}</div>`;
|
||||
Object.entries(details).forEach(([detail, paths]) => {
|
||||
paths.forEach(path => {
|
||||
const fileName = path.split('/').pop() || path;
|
||||
html += `<div class="file-item" data-path="${path}">${fileName}</div>`;
|
||||
});
|
||||
});
|
||||
});
|
||||
this.fileSidebar.innerHTML = html;
|
||||
|
||||
this.fileSidebar.querySelectorAll('.file-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
this.fileSidebar.querySelectorAll('.file-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
this.renderCurrentFile();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private selectFirstFile() {
|
||||
const firstItem = this.fileSidebar.querySelector('.file-item') as HTMLElement;
|
||||
if (firstItem) {
|
||||
firstItem.classList.add('active');
|
||||
this.renderCurrentFile();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/maps`);
|
||||
this.allMapConfig = await res.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to load config:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private renderCurrentFile() {
|
||||
const activeItem = this.fileSidebar.querySelector('.file-item.active') as HTMLElement;
|
||||
if (!activeItem) return;
|
||||
|
||||
this.currentPath = activeItem.dataset.path || '';
|
||||
this.boxes = this.allMapConfig[this.currentPath] || [];
|
||||
this.pathLabel.textContent = this.currentPath;
|
||||
this.img.src = this.currentPath;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
this.wrapper.addEventListener('mousedown', (e) => {
|
||||
if (e.button !== 0) return;
|
||||
this.isDrawing = true;
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
this.startX = e.clientX - rect.left;
|
||||
this.startY = e.clientY - rect.top;
|
||||
|
||||
this.currentBox = document.createElement('div');
|
||||
this.currentBox.className = 'draw-box';
|
||||
this.currentBox.style.left = this.startX + 'px';
|
||||
this.currentBox.style.top = this.startY + 'px';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'box-label';
|
||||
label.textContent = '#' + (this.boxes.length + 1);
|
||||
this.currentBox.appendChild(label);
|
||||
|
||||
this.wrapper.appendChild(this.currentBox);
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
if (!this.isDrawing || !this.currentBox) return;
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const currentX = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
||||
const currentY = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
|
||||
|
||||
const width = currentX - this.startX;
|
||||
const height = currentY - this.startY;
|
||||
|
||||
this.currentBox.style.width = Math.abs(width) + 'px';
|
||||
this.currentBox.style.height = Math.abs(height) + 'px';
|
||||
this.currentBox.style.left = (width > 0 ? this.startX : currentX) + 'px';
|
||||
this.currentBox.style.top = (height > 0 ? this.startY : currentY) + 'px';
|
||||
});
|
||||
|
||||
window.addEventListener('mouseup', () => {
|
||||
if (!this.isDrawing || !this.currentBox) return;
|
||||
this.isDrawing = false;
|
||||
|
||||
const width = parseFloat(this.currentBox.style.width);
|
||||
const height = parseFloat(this.currentBox.style.height);
|
||||
|
||||
if (width > 3 && height > 3) {
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const boxData = {
|
||||
x: (parseFloat(this.currentBox.style.left) / rect.width * 100).toFixed(2),
|
||||
y: (parseFloat(this.currentBox.style.top) / rect.height * 100).toFixed(2),
|
||||
w: (width / rect.width * 100).toFixed(2),
|
||||
h: (height / rect.height * 100).toFixed(2),
|
||||
asset_id: null
|
||||
};
|
||||
this.boxes.push(boxData);
|
||||
this.render();
|
||||
}
|
||||
|
||||
this.currentBox.remove();
|
||||
this.currentBox = null;
|
||||
});
|
||||
|
||||
(window as any).removeBox = (index: number) => {
|
||||
this.boxes.splice(index, 1);
|
||||
this.render();
|
||||
};
|
||||
|
||||
document.getElementById('btn-clear-all')?.addEventListener('click', () => {
|
||||
if(confirm('모든 박스를 삭제할까요?')) {
|
||||
this.boxes = [];
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btn-save-server')?.addEventListener('click', () => this.saveToServer());
|
||||
}
|
||||
|
||||
private async saveToServer() {
|
||||
if (!this.currentPath) return;
|
||||
|
||||
try {
|
||||
this.saveBtn.disabled = true;
|
||||
this.saveBtn.textContent = '저장 중...';
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/maps/save`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: this.currentPath, boxes: this.boxes })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
this.allMapConfig[this.currentPath] = [...this.boxes];
|
||||
this.statusEl.textContent = '✅ 서버 저장 완료 (' + new Date().toLocaleTimeString() + ')';
|
||||
setTimeout(() => this.statusEl.textContent = '', 3000);
|
||||
} else {
|
||||
alert('저장 실패!');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('서버 연결 오류!');
|
||||
} finally {
|
||||
this.saveBtn.disabled = false;
|
||||
this.saveBtn.textContent = '서버에 즉시 저장';
|
||||
}
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.boxListEl.innerHTML = '';
|
||||
const oldBoxes = this.wrapper.querySelectorAll('.placed-box');
|
||||
oldBoxes.forEach(b => b.remove());
|
||||
|
||||
this.boxes.forEach((box, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'placed-box';
|
||||
div.style.left = box.x + '%';
|
||||
div.style.top = box.y + '%';
|
||||
div.style.width = box.w + '%';
|
||||
div.style.height = box.h + '%';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'box-label';
|
||||
label.textContent = '#' + (i + 1);
|
||||
div.appendChild(label);
|
||||
|
||||
this.wrapper.appendChild(div);
|
||||
|
||||
// Create asset options dropdown
|
||||
let optionsHtml = '<option value="">-- 자산 매핑 안 됨 --</option>';
|
||||
this.assetOptions.forEach(opt => {
|
||||
const selected = box.asset_id === opt.id ? 'selected' : '';
|
||||
optionsHtml += `<option value="${opt.id}" ${selected}>${opt.name}</option>`;
|
||||
});
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'box-item';
|
||||
item.innerHTML = `
|
||||
<div class="box-header">
|
||||
<span class="box-index">#${i+1}</span>
|
||||
<button class="btn-del" onclick="removeBox(${i})">×</button>
|
||||
</div>
|
||||
<div class="box-inputs margin-bottom">
|
||||
<select data-index="${i}" data-prop="asset_id">
|
||||
${optionsHtml}
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-inputs">
|
||||
<div class="input-group">
|
||||
<label>X</label>
|
||||
<input type="number" step="0.01" value="${box.x}" data-index="${i}" data-prop="x">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>Y</label>
|
||||
<input type="number" step="0.01" value="${box.y}" data-index="${i}" data-prop="y">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>W</label>
|
||||
<input type="number" step="0.01" value="${box.w}" data-index="${i}" data-prop="w">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>H</label>
|
||||
<input type="number" step="0.01" value="${box.h}" data-index="${i}" data-prop="h">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.boxListEl.appendChild(item);
|
||||
});
|
||||
|
||||
// Add events to new inputs and selects
|
||||
this.boxListEl.querySelectorAll('input, select').forEach(input => {
|
||||
input.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement | HTMLSelectElement;
|
||||
const index = parseInt(target.dataset.index!);
|
||||
const prop = target.dataset.prop!;
|
||||
|
||||
if (this.boxes[index]) {
|
||||
if (prop === 'asset_id') {
|
||||
this.boxes[index][prop] = target.value || null;
|
||||
} else {
|
||||
this.boxes[index][prop] = parseFloat(target.value).toFixed(2);
|
||||
this.render(); // Re-render to update the map visual size
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
import { state } from '../core/state';
|
||||
import { renderPcList } from './List/PcListView';
|
||||
import { renderServerList } from './List/ServerListView';
|
||||
import { renderStorageList } from './List/StorageListView';
|
||||
import { renderEquipmentList } from './List/EquipmentListView';
|
||||
import { renderMobileList } from './List/MobileListView';
|
||||
import { renderSwList } from './List/SwListView';
|
||||
import { renderCloudList } from './List/CloudListView';
|
||||
import { renderDomainList } from './List/DomainListView';
|
||||
import { renderNetworkList } from './List/NetworkListView';
|
||||
import { renderPcPartList } from './List/PcPartListView';
|
||||
import { renderPartsMasterList } from './List/PartsMasterListView';
|
||||
import { renderSpaceInfoList } from './List/SpaceInfoListView';
|
||||
import { renderGiftList } from './List/GiftListView';
|
||||
import { renderFacilityList } from './List/FacilityListView';
|
||||
import { renderCostList } from './List/CostListView';
|
||||
import { renderUserList } from './List/UserListView';
|
||||
import { createIcons, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw, BookOpen, Settings } from 'lucide';
|
||||
|
||||
/**
|
||||
* 자산 목록 테이블 렌더링 통합 허브 (Vercel Style Normalized)
|
||||
*/
|
||||
export function renderSWTable(mainContent: HTMLElement) {
|
||||
if (!mainContent) return;
|
||||
console.log(`📂 Rendering Table for: ${state.activeCategory} / ${state.activeSubTab}`);
|
||||
|
||||
mainContent.innerHTML = `
|
||||
<div class="view-content-wrapper">
|
||||
<div id="list-view-container" style="flex: 1; display: flex; flex-direction: column; overflow: hidden;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const container = document.getElementById('list-view-container')!;
|
||||
|
||||
try {
|
||||
const tab = state.activeSubTab;
|
||||
|
||||
if (state.activeCategory === 'hw') {
|
||||
if (tab === 'PC') renderPcList(container);
|
||||
else if (tab === '서버') renderServerList(container);
|
||||
else if (tab === '스토리지') renderStorageList(container);
|
||||
else if (tab === '업무지원장비') renderEquipmentList(container);
|
||||
else if (tab === '네트워크') renderNetworkList(container);
|
||||
else if (tab === 'PC부품') renderPcPartList(container);
|
||||
else if (tab === '부품 마스터') renderPartsMasterList(container);
|
||||
else if (tab === '공간정보장비') renderSpaceInfoList(container);
|
||||
else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 하드웨어 리스트 뷰가 정의되지 않았습니다.</div>`;
|
||||
}
|
||||
} else if (state.activeCategory === 'sw') {
|
||||
if (tab === '외부SW' || tab === '내부SW') {
|
||||
renderSwList(container);
|
||||
} else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 소프트웨어 리스트 뷰가 정의되지 않았습니다.</div>`;
|
||||
}
|
||||
} else if (state.activeCategory === 'ops') {
|
||||
if (tab === '도메인') renderDomainList(container);
|
||||
else if (tab === '클라우드') renderCloudList(container);
|
||||
else if (tab === '비용관리') renderCostList(container);
|
||||
else if (tab === '사용자') renderUserList(container);
|
||||
else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 운영지원 리스트 뷰가 정의되지 않았습니다.</div>`;
|
||||
}
|
||||
} else if (state.activeCategory === 'vip') {
|
||||
if (tab === '선물') renderGiftList(container);
|
||||
else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted); text-align:center; margin-top:3rem;">내빈/외빈(${tab}) 관리 기능은 현재 준비 중입니다.</div>`;
|
||||
}
|
||||
} else if (state.activeCategory === 'fac') {
|
||||
if (tab === '사무가구') renderFacilityList(container);
|
||||
else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted); text-align:center; margin-top:3rem;">시설자산(${tab}) 관리 기능은 현재 준비 중입니다.</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 아이콘 초기화
|
||||
createIcons({
|
||||
icons: { Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw, BookOpen, Settings }
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('❌ Error rendering table view:', err);
|
||||
mainContent.innerHTML = `<div style="padding:2rem; color:var(--danger);">목록을 불러오는 중 오류가 발생했습니다: ${err?.message || err}</div>`;
|
||||
}
|
||||
}
|
||||
import { state } from '../core/state';
|
||||
import { renderPcList } from './List/PcListView';
|
||||
import { renderServerList } from './List/ServerListView';
|
||||
import { renderStorageList } from './List/StorageListView';
|
||||
import { renderEquipmentList } from './List/EquipmentListView';
|
||||
import { renderMobileList } from './List/MobileListView';
|
||||
import { renderSwList } from './List/SwListView';
|
||||
import { renderCloudList } from './List/CloudListView';
|
||||
import { renderDomainList } from './List/DomainListView';
|
||||
import { renderNetworkList } from './List/NetworkListView';
|
||||
import { renderPcPartList } from './List/PcPartListView';
|
||||
import { renderPartsMasterList } from './List/PartsMasterListView';
|
||||
import { renderSpaceInfoList } from './List/SpaceInfoListView';
|
||||
import { renderGiftList } from './List/GiftListView';
|
||||
import { renderFacilityList } from './List/FacilityListView';
|
||||
import { renderCostList } from './List/CostListView';
|
||||
import { renderUserList } from './List/UserListView';
|
||||
import { createIcons, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw, BookOpen, Settings } from 'lucide';
|
||||
|
||||
/**
|
||||
* 자산 목록 테이블 렌더링 통합 허브 (Vercel Style Normalized)
|
||||
*/
|
||||
export function renderSWTable(mainContent: HTMLElement) {
|
||||
if (!mainContent) return;
|
||||
console.log(`📂 Rendering Table for: ${state.activeCategory} / ${state.activeSubTab}`);
|
||||
|
||||
mainContent.innerHTML = `
|
||||
<div class="view-content-wrapper">
|
||||
<div id="list-view-container" style="flex: 1; display: flex; flex-direction: column; overflow: hidden;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const container = document.getElementById('list-view-container')!;
|
||||
|
||||
try {
|
||||
const tab = state.activeSubTab;
|
||||
|
||||
if (state.activeCategory === 'hw') {
|
||||
if (tab === 'PC') renderPcList(container);
|
||||
else if (tab === '서버') renderServerList(container);
|
||||
else if (tab === '스토리지') renderStorageList(container);
|
||||
else if (tab === '업무지원장비') renderEquipmentList(container);
|
||||
else if (tab === '네트워크') renderNetworkList(container);
|
||||
else if (tab === 'PC부품') renderPcPartList(container);
|
||||
else if (tab === '부품 마스터') renderPartsMasterList(container);
|
||||
else if (tab === '공간정보장비') renderSpaceInfoList(container);
|
||||
else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 하드웨어 리스트 뷰가 정의되지 않았습니다.</div>`;
|
||||
}
|
||||
} else if (state.activeCategory === 'sw') {
|
||||
if (tab === '외부SW' || tab === '내부SW') {
|
||||
renderSwList(container);
|
||||
} else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 소프트웨어 리스트 뷰가 정의되지 않았습니다.</div>`;
|
||||
}
|
||||
} else if (state.activeCategory === 'ops') {
|
||||
if (tab === '도메인') renderDomainList(container);
|
||||
else if (tab === '클라우드') renderCloudList(container);
|
||||
else if (tab === '비용관리') renderCostList(container);
|
||||
else if (tab === '사용자') renderUserList(container);
|
||||
else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted);">"${tab}" 탭에 대한 운영지원 리스트 뷰가 정의되지 않았습니다.</div>`;
|
||||
}
|
||||
} else if (state.activeCategory === 'vip') {
|
||||
if (tab === '선물') renderGiftList(container);
|
||||
else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted); text-align:center; margin-top:3rem;">내빈/외빈(${tab}) 관리 기능은 현재 준비 중입니다.</div>`;
|
||||
}
|
||||
} else if (state.activeCategory === 'fac') {
|
||||
if (tab === '사무가구') renderFacilityList(container);
|
||||
else {
|
||||
container.innerHTML = `<div style="padding:2rem; color:var(--text-muted); text-align:center; margin-top:3rem;">시설자산(${tab}) 관리 기능은 현재 준비 중입니다.</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 아이콘 초기화
|
||||
createIcons({
|
||||
icons: { Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, RefreshCcw, BookOpen, Settings }
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('❌ Error rendering table view:', err);
|
||||
mainContent.innerHTML = `<div style="padding:2rem; color:var(--danger);">목록을 불러오는 중 오류가 발생했습니다: ${err?.message || err}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,236 +1,236 @@
|
||||
/* ITAM Map Coordinate Editor Styles */
|
||||
|
||||
.file-sidebar {
|
||||
width: 260px;
|
||||
background: var(--white);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.folder-item {
|
||||
padding: 10px 15px;
|
||||
background: var(--bg-light);
|
||||
font-weight: 700;
|
||||
font-size: var(--fs-base);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.file-item {
|
||||
padding: 8px 25px;
|
||||
cursor: pointer;
|
||||
font-size: var(--fs-sm);
|
||||
border-bottom: 1px solid var(--bg-color);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.file-item:hover { background: var(--bg-light); }
|
||||
.file-item.active { background: var(--primary-color); color: var(--white); font-weight: 700; }
|
||||
|
||||
/* Center: Editor Area */
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #e0e0e0; /* 전용 배경색 유지 */
|
||||
}
|
||||
|
||||
.img-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 30px rgba(0,0,0,0.3);
|
||||
background: var(--white);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.img-wrapper img {
|
||||
display: block;
|
||||
max-width: calc(100vw - 650px);
|
||||
max-height: 85vh;
|
||||
width: auto;
|
||||
height: auto;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
/* Right Sidebar: Control Panel */
|
||||
.sidebar {
|
||||
width: 350px;
|
||||
background: var(--white);
|
||||
border-left: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
box-shadow: -5px 0 15px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.sidebar h2 { margin-top: 0; color: var(--primary-color); font-size: var(--fs-lg); font-weight: 600; }
|
||||
.sidebar p { font-size: var(--fs-sm); color: var(--text-muted); line-height: 1.4; margin-bottom: 20px; }
|
||||
|
||||
.current-path { font-size: var(--fs-xs); color: var(--text-muted); margin-bottom: 10px; word-break: break-all; font-family: monospace; }
|
||||
|
||||
.box-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background: var(--bg-light);
|
||||
}
|
||||
|
||||
.box-item {
|
||||
font-family: monospace;
|
||||
font-size: var(--fs-xs);
|
||||
padding: 10px 6px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.box-index {
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.box-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
color: var(--text-muted);
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
font-size: var(--fs-xs);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.box-item:hover { background: var(--white); }
|
||||
.btn-del { cursor: pointer; color: var(--danger); border: none; background: none; font-size: var(--fs-md); padding: 0 5px; }
|
||||
|
||||
.actions { display: flex; flex-direction: column; gap: 8px; }
|
||||
|
||||
/* Drawing Elements */
|
||||
.draw-box {
|
||||
position: absolute;
|
||||
border: 2px solid var(--edit-mode-color);
|
||||
background: rgba(255, 61, 0, 0.2);
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.placed-box {
|
||||
position: absolute;
|
||||
border: 1.5px solid var(--primary-color);
|
||||
background: rgba(30, 81, 73, 0.15);
|
||||
cursor: pointer;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.placed-box:hover {
|
||||
background: rgba(30, 81, 73, 0.4);
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.placed-box.selected {
|
||||
border: 2.5px solid var(--edit-mode-color);
|
||||
z-index: 60;
|
||||
box-shadow: 0 0 10px rgba(255,61,0,0.5);
|
||||
}
|
||||
|
||||
.box-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: var(--fs-xs);
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
background: rgba(255,255,255,0.7);
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.draw-box .box-label {
|
||||
color: var(--edit-mode-color);
|
||||
background: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
#save-status {
|
||||
margin-top: 8px;
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--success);
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* Editor Body & Layout Overrides */
|
||||
.editor-body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-version {
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.actions .btn {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
/* Box Item Dropdown Inputs */
|
||||
.box-inputs.margin-bottom {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.box-inputs select {
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: var(--fs-xs);
|
||||
background-color: var(--canvas);
|
||||
color: var(--text-main);
|
||||
outline: none;
|
||||
}
|
||||
.box-inputs select:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
/* ITAM Map Coordinate Editor Styles */
|
||||
|
||||
.file-sidebar {
|
||||
width: 260px;
|
||||
background: var(--white);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.folder-item {
|
||||
padding: 10px 15px;
|
||||
background: var(--bg-light);
|
||||
font-weight: 700;
|
||||
font-size: var(--fs-base);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.file-item {
|
||||
padding: 8px 25px;
|
||||
cursor: pointer;
|
||||
font-size: var(--fs-sm);
|
||||
border-bottom: 1px solid var(--bg-color);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.file-item:hover { background: var(--bg-light); }
|
||||
.file-item.active { background: var(--primary-color); color: var(--white); font-weight: 700; }
|
||||
|
||||
/* Center: Editor Area */
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #e0e0e0; /* 전용 배경색 유지 */
|
||||
}
|
||||
|
||||
.img-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 30px rgba(0,0,0,0.3);
|
||||
background: var(--white);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.img-wrapper img {
|
||||
display: block;
|
||||
max-width: calc(100vw - 650px);
|
||||
max-height: 85vh;
|
||||
width: auto;
|
||||
height: auto;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
/* Right Sidebar: Control Panel */
|
||||
.sidebar {
|
||||
width: 350px;
|
||||
background: var(--white);
|
||||
border-left: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
box-shadow: -5px 0 15px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.sidebar h2 { margin-top: 0; color: var(--primary-color); font-size: var(--fs-lg); font-weight: 600; }
|
||||
.sidebar p { font-size: var(--fs-sm); color: var(--text-muted); line-height: 1.4; margin-bottom: 20px; }
|
||||
|
||||
.current-path { font-size: var(--fs-xs); color: var(--text-muted); margin-bottom: 10px; word-break: break-all; font-family: monospace; }
|
||||
|
||||
.box-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background: var(--bg-light);
|
||||
}
|
||||
|
||||
.box-item {
|
||||
font-family: monospace;
|
||||
font-size: var(--fs-xs);
|
||||
padding: 10px 6px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.box-index {
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.box-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
color: var(--text-muted);
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
font-size: var(--fs-xs);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.box-item:hover { background: var(--white); }
|
||||
.btn-del { cursor: pointer; color: var(--danger); border: none; background: none; font-size: var(--fs-md); padding: 0 5px; }
|
||||
|
||||
.actions { display: flex; flex-direction: column; gap: 8px; }
|
||||
|
||||
/* Drawing Elements */
|
||||
.draw-box {
|
||||
position: absolute;
|
||||
border: 2px solid var(--edit-mode-color);
|
||||
background: rgba(255, 61, 0, 0.2);
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.placed-box {
|
||||
position: absolute;
|
||||
border: 1.5px solid var(--primary-color);
|
||||
background: rgba(30, 81, 73, 0.15);
|
||||
cursor: pointer;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.placed-box:hover {
|
||||
background: rgba(30, 81, 73, 0.4);
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.placed-box.selected {
|
||||
border: 2.5px solid var(--edit-mode-color);
|
||||
z-index: 60;
|
||||
box-shadow: 0 0 10px rgba(255,61,0,0.5);
|
||||
}
|
||||
|
||||
.box-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: var(--fs-xs);
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
background: rgba(255,255,255,0.7);
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.draw-box .box-label {
|
||||
color: var(--edit-mode-color);
|
||||
background: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
#save-status {
|
||||
margin-top: 8px;
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--success);
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* Editor Body & Layout Overrides */
|
||||
.editor-body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-version {
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.actions .btn {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
/* Box Item Dropdown Inputs */
|
||||
.box-inputs.margin-bottom {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.box-inputs select {
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: var(--fs-xs);
|
||||
background-color: var(--canvas);
|
||||
color: var(--text-main);
|
||||
outline: none;
|
||||
}
|
||||
.box-inputs select:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user