diff --git a/image 92.png b/image 92.png new file mode 100644 index 0000000..5a36e8f Binary files /dev/null and b/image 92.png differ diff --git a/index.html b/index.html index 611f437..378e833 100644 --- a/index.html +++ b/index.html @@ -1,61 +1,72 @@ - - - - - ITAM 자산관리 ERP - - - - - - - - - - -
- -
- +
+ + +
+ +
+ + + +
+ + + + + + \ No newline at end of file diff --git a/server.js b/server.js index c5d0b95..60f0baa 100644 --- a/server.js +++ b/server.js @@ -370,7 +370,7 @@ app.get('/api/generate-asset-code', async (req, res) => { }); } - const nextNum = (maxNum + 1).toString().padStart(3, '0'); + const nextNum = (maxNum + 1).toString().padStart(4, '0'); res.json({ nextCode: `${prefix}${nextNum}` }); } catch (err) { res.status(500).json({ error: err.message }); diff --git a/src/components/Guide.ts b/src/components/Guide.ts index ef643e7..932ef18 100644 --- a/src/components/Guide.ts +++ b/src/components/Guide.ts @@ -1,6 +1,7 @@ import { createIcons, BookOpen, X, ChevronDown, ChevronRight, RefreshCw } from 'lucide'; +import { state } from '../core/state'; -// ─── 자산별 가이드 콘텐츠 정의 ─── +// ─── 자산별 가이드 콘텐츠 정의 (SW_Table 브랜치 전체 복구) ─── interface GuideTabConfig { id: string; label: string; @@ -46,6 +47,21 @@ const GUIDE_TABS: GuideTabConfig[] = [ + +
+

시스템 기본 사용방법

+ + + + + + + + + + +
기능방법
자산 조회상단 카테고리(하드웨어/소프트웨어) 및 하위 탭 선택 후 데이터 조회
자산 등록[자산 추가] 버튼 클릭 후 상세 정보 입력 및 저장
정보 수정목록 행 클릭 후 나타나는 모달에서 내용 변경 및 저장
엑셀 업로드[업로드] 버튼 선택 후 표준 양식의 .xlsx 파일 선택
전체 엑셀저장[엑셀저장] 버튼 클릭 시 현재 전체 자산 데이터를 Excel로 백업
표준 양식[양식] 버튼 클릭 시 데이터 업로드용 빈 양식 다운로드
+
` }, { @@ -55,7 +71,7 @@ const GUIDE_TABS: GuideTabConfig[] = [

개인PC 관리 가이드

- 개인PC는 임직원에게 지급되는 데스크톱 및 노트북을 관리합니다. 자산의 지급, 교체, 반납까지의 전체 생애주기를 시스템에서 추적합니다. + 임직원에게 지급되는 데스크톱 및 노트북을 관리합니다. 자산의 지급, 교체, 반납까지의 전체 생애주기를 시스템에서 추적합니다.

@@ -70,33 +86,63 @@ const GUIDE_TABS: GuideTabConfig[] = [
2 -
자산 등록

자산코드 부여, 사양(CPU/RAM/Storage) 등록

+
자산 등록

자산번호 부여, 상세 사양 등록

3 -
사용자 지급

사용자·사용조직 지정, 설치위치 기록

+
사용자 지급

사용자 지정 및 설치위치 기록

4 -
운영 관리

OS 업데이트, 보안 점검, 품의서 관리

+
운영 관리

보안 점검 및 수리 이력 관리

+
+
+ +
+
+ 5 +
교체/반납

장비 회수 및 데이터 소거

+
+ +
+ 6 +
폐기 처리

불용 처리 및 매각/폐기 등록

+ +
+

주요 관리 항목

+ + + + + + + + + +
항목설명관리 주기
구매법인자산의 소유 법인등록 시
사용자/조직실제 사용자 및 소속 부서변동 시
자산번호고유 식별 번호 (바코드)등록 시
모델명/사양제조사 모델 및 CPU/RAM 등등록 시
도입금액구매 비용 (부가세 포함)등록 시
+
+ +
+ 관리 팁: 자산 이력에서 '분출'과 '반납' 로그를 꼼꼼히 기록하면 자산의 실제 위치를 정확히 파악할 수 있습니다. +
` }, { id: 'server', - label: '🖥️ 서버', + label: '🖥️ 서버/스토리지', content: `
-

서버 관리 가이드

+

인프라 자산 관리 가이드

- 물리 서버와 가상 서버를 포함한 서버급 자산을 관리합니다. 안정적인 서비스 운영을 위해 체계적인 관리가 필요합니다. + 서버실 및 IDC에 설치된 물리 서버와 스토리지 장비를 관리합니다. 고가의 자산이므로 담당자(정/부) 지정이 필수입니다.

@@ -106,21 +152,66 @@ const GUIDE_TABS: GuideTabConfig[] = [
1 -
도입 계획

용도 정의, 사양 산정, 구매 승인

+
도입 계획

사양 확정 및 구매 승인

2 -
설치 및 등록

랙 배치, 네트워크 설정, 자산 등록

+
설치 및 등록

네트워크 설정 및 자산번호 부여

3 -
운영 관리

모니터링, 패치 적용, 장애 대응

+
운영 관리

정기 점검 및 장애 이력 관리

+ +
+

필수 입력 항목

+ + + + + + + + +
항목중요성
IP 주소서버 접속 및 모니터링을 위한 필수 정보
설치위치IDC 또는 서버실 내의 정확한 랙 위치
담당자(정/부)비상 시 연락 가능한 관리 책임자
용도/상세운영 중인 서비스 및 상세 업무 설명
+
+ +
+ 주의 사항: 서버 자산의 IP가 변경될 경우 시스템에 즉시 반영하여 네트워크 관리 대장과의 정합성을 유지해야 합니다. +
+ ` + }, + { + id: 'software', + label: '💾 소프트웨어', + content: ` +
+

소프트웨어 자산 관리 가이드

+

+ 구독형(SaaS) 및 영구형 라이선스를 관리합니다. 불법 소프트웨어 사용 방지와 비용 최적화가 주 목적입니다. +

+
+ +
+

라이선스 관리 포인트

+ + + + + + + +
구분관리 내용
구독형(Sub)구독 만료일 도래 전 갱신 여부 결정 및 비용 정산
영구형(Perm)보유 수량 대비 실제 설치 수량 매핑 (초과 사용 금지)
운영서비스도메인, 메일 등 매월 또는 매년 발생하는 비용 추적
+
+ +
+ 팁: 소프트웨어 상세 페이지의 [사용자 할당] 기능을 활용하여 누가 어떤 라이선스를 사용하는지 체계적으로 관리하세요. +
` } ]; @@ -131,7 +222,7 @@ export function initGuide() { if (document.getElementById('guide-overlay')) return; const overlay = document.createElement('div'); - overlay.className = 'guide-overlay'; + overlay.className = 'modal-overlay hidden'; overlay.id = 'guide-overlay'; const tabsHtml = GUIDE_TABS.map((tab, i) => @@ -143,32 +234,33 @@ export function initGuide() { ).join(''); overlay.innerHTML = ` -
-
-

IT 자산관리 프로세스 가이드

-
-
${tabsHtml}
-
${panelsHtml}
+
+
${tabsHtml}
+
+
`; body.appendChild(overlay); const openGuide = () => { - console.log('📖 Opening Guide Modal...'); - overlay.classList.add('active'); + console.log('📖 Opening Full Guide Modal...'); + overlay.classList.remove('hidden'); }; - const closeGuide = () => overlay.classList.remove('active'); + const closeGuide = () => overlay.classList.add('hidden'); const triggerBtn = document.getElementById('btn-open-guide-header'); if (triggerBtn) { - console.log('✅ Guide trigger button found and bound.'); triggerBtn.addEventListener('click', openGuide); - } else { - console.warn('⚠️ Guide trigger button (#btn-open-guide-header) not found in DOM.'); } overlay.addEventListener('click', (e) => { if (e.target === overlay) closeGuide(); }); @@ -187,5 +279,5 @@ export function initGuide() { }); }); - createIcons({ icons: { BookOpen, X, ChevronDown, ChevronRight, RefreshCw }, nameAttr: 'data-lucide' }); + createIcons({ icons: { BookOpen, X, ChevronDown, ChevronRight, RefreshCw } }); } diff --git a/src/components/Modal/CloudModal.ts b/src/components/Modal/CloudModal.ts index eba98e1..97b7ed2 100644 --- a/src/components/Modal/CloudModal.ts +++ b/src/components/Modal/CloudModal.ts @@ -221,7 +221,7 @@ export function initCloudModal(renderContent: () => void, closeModals: () => voi id: Math.random().toString(36).substring(2, 9), assetId: newAsset.id, date: `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`, - user: '관리자', + user: '담당자', details: '신규 등록' }); } @@ -271,7 +271,7 @@ export function initCloudModal(renderContent: () => void, closeModals: () => voi id: Math.random().toString(36).substring(2, 9), assetId: id, date, - user: '관리자', + user: '담당자', details }); diff --git a/src/components/Modal/HWModal.ts b/src/components/Modal/HWModal.ts index 005e51b..b87c03f 100644 --- a/src/components/Modal/HWModal.ts +++ b/src/components/Modal/HWModal.ts @@ -1,6 +1,7 @@ import { state, saveHardwareAsset, deleteHardwareAsset } from '../../core/state'; -import { HardwareAsset, HardwareLog } from '../../core/excelHandler'; +import { HardwareAsset } from '../../core/excelHandler'; import { closeModals } from './BaseModal'; +import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Paperclip } from 'lucide'; import { CORP_LIST, ORG_LIST, HW_TYPE_LIST, LOCATION_DATA, TYPE_PREFIX_MAP } from './SharedData'; import { @@ -21,62 +22,64 @@ let isEditMode = false; const STATUS_LIST = ['대여중', '보관중', '수리중', '기타']; -// 필드 ID ↔ 데이터 Key 매핑 (유지보수 시 이 부분만 수정) +/** + * 하드웨어 필드 매핑 (통합 스키마 기반) + */ const HW_FIELD_MAP: Record = { - '유형': 'type', - '법인': '법인', - '자산코드': '자산코드', - '현사용조직': '현사용조직', - '이전사용조직': '이전사용조직', - '상세용도': '상세용도', - '모델명': '모델명', - '명칭': '명칭', - '보관위치': '보관위치', - '현재상태': '현재상태', - 'IP주소': 'IP주소', - 'IP2': 'IP2', - '원격접속': '원격접속', - '서버ID': '서버ID', - '서버PW': '서버PW', - '모니터링': '모니터링', - 'OS': 'OS', - 'CPU': 'CPU', - 'RAM': 'RAM', - 'SSD1': 'SSD1', - 'SSD2': 'SSD2', - 'HW사양': 'HW사양', - '담당자_정': '담당자_정', - '담당자_부': '담당자_부', - '구매일': '구매연월', - '금액': '금액', - '비고': '비고', - '사용자': '사용자' + '유형': ASSET_SCHEMA.TYPE.key, + '법인': ASSET_SCHEMA.CORP.key, + '자산코드': ASSET_SCHEMA.ASSET_CODE.key, + '현사용조직': ASSET_SCHEMA.ORG.key, + '이전사용조직': ASSET_SCHEMA.PREV_ORG.key, + '상세용도': '상세용도', + '모델명': ASSET_SCHEMA.MODEL.key, + '메인보드': ASSET_SCHEMA.MAINBOARD.key, + '명칭': '명칭', + '보관위치': ASSET_SCHEMA.STORE_LOC.key, + '현재상태': ASSET_SCHEMA.STATUS.key, + 'IP주소': ASSET_SCHEMA.IP_ADDR.key, + 'IP2': ASSET_SCHEMA.IP_ADDR2.key, + '원격접속': '원격접속', + '서버ID': '서버ID', + '서버PW': '서버PW', + '모니터링': '모니터링', + 'OS': ASSET_SCHEMA.OS.key, + 'CPU': ASSET_SCHEMA.CPU.key, + 'RAM': ASSET_SCHEMA.RAM.key, + 'SSD1': ASSET_SCHEMA.STORAGE1.key, + 'SSD2': ASSET_SCHEMA.STORAGE2.key, + 'HW사양': 'HW사양', + '담당자_정': ASSET_SCHEMA.MANAGER_MAIN.key, + '담당자_부': ASSET_SCHEMA.MANAGER_SUB.key, + '구매일': ASSET_SCHEMA.PURCHASE_YM.key, + '금액': ASSET_SCHEMA.PRICE.key, + '비고': ASSET_SCHEMA.REMARKS.key, + '사용자': ASSET_SCHEMA.USER.key }; const HW_FORM_HTML = ` -
기본 정보 (Identity)
- +
- +
- +
- +
- +
@@ -94,29 +97,30 @@ const HW_FORM_HTML = `
운영 및 상태 관리
- +
- +
네트워크 정보 (Connectivity)
-
-
+
+
-
+
시스템 사양 (Specifications)
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -124,13 +128,13 @@ const HW_FORM_HTML = `
-
-
-
-
-
+
+
+
+
+
- +
@@ -162,6 +166,7 @@ function applyTypeSpecificUI(type: string) { specTitle: document.getElementById('hw-spec-title'), opTitle: document.getElementById('hw-op-title'), model: document.getElementById('hw-model-group'), + mainboard: document.getElementById('hw-mainboard-group'), os: document.getElementById('hw-os-group'), cpu: document.getElementById('hw-cpu-group'), ram: document.getElementById('hw-ram-group'), @@ -177,7 +182,6 @@ function applyTypeSpecificUI(type: string) { const opOnly = document.querySelectorAll('.op-only'); const standardLoc = document.querySelectorAll('.loc-standard'); - // 초기화 serverOnly.forEach(el => (el as HTMLElement).style.display = 'none'); nonServer.forEach(el => (el as HTMLElement).style.display = 'none'); opOnly.forEach(el => (el as HTMLElement).style.display = 'none'); @@ -187,9 +191,9 @@ function applyTypeSpecificUI(type: string) { const osLabel = document.querySelector('label[for="hw-OS"]') as HTMLElement; const ramLabel = document.querySelector('label[for="hw-RAM"]') as HTMLElement; const modelLabel = document.querySelector('label[for="hw-모델명"]') as HTMLElement; - if (osLabel) osLabel.innerText = '운영체제 (OS)'; - if (ramLabel) ramLabel.innerText = 'RAM 용량'; - if (modelLabel) modelLabel.innerText = '모델명'; + if (osLabel) osLabel.innerText = ASSET_SCHEMA.OS.ui; + if (ramLabel) ramLabel.innerText = ASSET_SCHEMA.RAM.ui; + if (modelLabel) modelLabel.innerText = ASSET_SCHEMA.MODEL.ui; const isMobileGroup = ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t)); const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품'); @@ -208,7 +212,6 @@ function applyTypeSpecificUI(type: string) { if (groups.os && osLabel) { osLabel.innerText = '출시연월'; groups.os.style.display = 'flex'; } } else if (['RAM', 'HDD'].some(t => upperType.includes(t))) { if (groups.ram && ramLabel) { ramLabel.innerText = '용량'; groups.ram.style.display = 'flex'; } - if (upperType.includes('HDD') && modelLabel) modelLabel.innerText = 'S/N'; } else { if (groups.hwSpec) groups.hwSpec.style.display = 'flex'; } @@ -216,8 +219,8 @@ function applyTypeSpecificUI(type: string) { else if (isPcType) { if (groups.user) groups.user.style.display = 'flex'; if (groups.specTitle) groups.specTitle.style.display = 'flex'; + if (groups.mainboard) groups.mainboard.style.display = 'flex'; - // 노트북은 상세유형 선택창 숨김 if (upperType === '노트북') { if (groups.detailPurpose) groups.detailPurpose.style.display = 'none'; nonServer.forEach(el => (el as HTMLElement).style.display = 'flex'); @@ -254,13 +257,11 @@ export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view') }); isEditMode = (mode === 'add'); - - // 데이터 채우기 (자동 매핑) autoFillForm('hw', asset, HW_FIELD_MAP); - setFieldValue('hw-명칭', asset.명칭 || asset.모델명); - if (!asset.구매연월 && asset.구매일) setFieldValue('hw-구매일', asset.구매일); + setFieldValue('hw-명칭', asset.명칭 || asset[ASSET_SCHEMA.MODEL.key]); + if (!asset[ASSET_SCHEMA.PURCHASE_YM.key] && asset.구매일) setFieldValue('hw-구매일', asset.구매일); - parseAndSetLocation(asset.위치, 'hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타'); + parseAndSetLocation(asset[ASSET_SCHEMA.LOCATION.key], 'hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타'); applyTypeSpecificUI(asset.type); renderHwHistory(asset.id); @@ -276,18 +277,17 @@ export function initHwModal(onSave: () => void, closeModalsCb: () => void) { }); document.body.insertAdjacentHTML('beforeend', html); - // 이력 추가 모달 HTML도 함께 추가 const logModalHTML = ` `; @@ -338,15 +338,12 @@ export function initHwModal(onSave: () => void, closeModalsCb: () => void) { } catch (err) { alert('자산번호 생성에 실패했습니다.'); } }); - // YYYYMM 입력 제한 로직 (숫자 6자리) ['hw-구매일', 'hw-OS'].forEach(id => { const el = document.getElementById(id) as HTMLInputElement; el?.addEventListener('input', (e) => { const target = e.target as HTMLInputElement; const label = document.querySelector(`label[for="${id}"]`) as HTMLElement; - // OS 필드의 경우 라벨이 '출시연월'일 때만 숫자 제한 적용 if (id === 'hw-OS' && label?.innerText !== '출시연월') return; - target.value = target.value.replace(/[^0-9]/g, '').substring(0, 6); }); }); @@ -365,10 +362,8 @@ export function initHwModal(onSave: () => void, closeModalsCb: () => void) { return; } - // 데이터 추출 (자동 매핑) const extracted = autoExtractForm('hw', HW_FIELD_MAP); - - if (!extracted.자산코드) { + if (!extracted[ASSET_SCHEMA.ASSET_CODE.key]) { alert('자산번호가 없습니다. [생성] 버튼을 눌러 자산번호를 먼저 부여해주세요.'); return; } @@ -376,51 +371,52 @@ export function initHwModal(onSave: () => void, closeModalsCb: () => void) { const upperType = (extracted.type || '').toUpperCase(); const isOpType = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품') || ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t)); - // --- 자동 변경 이력 생성 로직 --- - // 모든 하드웨어 유형에 대해 자동 로깅 적용 if (HW_TYPE_LIST.includes(extracted.type) || extracted.type === '개인PC') { const diffLogs: string[] = []; const compareFields = [ - { key: '현사용조직', label: '현사용조직' }, - { key: '위치', label: '설치위치' }, - { key: '관리자', label: '담당자' }, - { key: '현재상태', label: '상태' }, - { key: 'IP주소', label: 'IP' }, + { key: ASSET_SCHEMA.ORG.key, label: ASSET_SCHEMA.ORG.ui }, + { key: ASSET_SCHEMA.LOCATION.key, label: ASSET_SCHEMA.LOCATION.ui }, + { key: ASSET_SCHEMA.MANAGER_MAIN.key, label: '담당자' }, + { key: ASSET_SCHEMA.STATUS.key, label: ASSET_SCHEMA.STATUS.ui }, + { key: ASSET_SCHEMA.IP_ADDR.key, label: ASSET_SCHEMA.IP_ADDR.ui }, { key: '상세용도', label: '상세유형' }, - { key: '모델명', label: '모델명' } + { key: ASSET_SCHEMA.MODEL.key, label: ASSET_SCHEMA.MODEL.ui } ]; - const currentIp = currentAsset.IP주소 || ''; - const newIp = getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'); - const currentLocation = currentAsset.위치 || ''; - const newLocation = isOpType ? extracted.보관위치 : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타'); + const isNewAsset = !currentAsset || !currentAsset.자산코드; - compareFields.forEach(f => { - let oldVal = ''; - let newVal = ''; + if (isNewAsset) { + diffLogs.push('자산 신규 등록'); + } else { + const newIp = String(getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server') || '').trim(); + const newLocation = String(isOpType ? extracted[ASSET_SCHEMA.STORE_LOC.key] : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타') || '').trim(); - if (f.key === 'IP주소') { - oldVal = currentIp; - newVal = newIp; - } else if (f.key === '위치') { - oldVal = currentLocation; - newVal = newLocation; - } else if (f.key === '관리자') { - oldVal = currentAsset.담당자_정 || ''; - newVal = extracted.담당자_정 || ''; - } else if (f.key === '상세용도') { - oldVal = currentAsset.상세용도 || ''; - // 비 PC 자산은 유형을 상세유형으로 간주 - newVal = (extracted.type !== 'PC' && extracted.type !== '개인PC') ? extracted.type : (extracted.상세용도 || ''); - } else { - oldVal = (currentAsset as any)[f.key] || ''; - newVal = extracted[f.key] || ''; - } + compareFields.forEach(f => { + let oldVal = ''; + let newVal = ''; - if (oldVal !== newVal) { - diffLogs.push(`${f.label}: ${oldVal || '(없음)'} → ${newVal || '(없음)'}`); - } - }); + if (f.key === ASSET_SCHEMA.IP_ADDR.key) { + oldVal = String(currentAsset[ASSET_SCHEMA.IP_ADDR.key] || '').trim(); + newVal = newIp; + } else if (f.key === ASSET_SCHEMA.LOCATION.key) { + oldVal = String(currentAsset[ASSET_SCHEMA.LOCATION.key] || '').trim(); + newVal = newLocation; + } else if (f.key === ASSET_SCHEMA.MANAGER_MAIN.key) { + oldVal = String(currentAsset[ASSET_SCHEMA.MANAGER_MAIN.key] || '').trim(); + newVal = String(extracted[ASSET_SCHEMA.MANAGER_MAIN.key] || '').trim(); + } else if (f.key === '상세용도') { + oldVal = String(currentAsset.상세용도 || '').trim(); + newVal = String((extracted.type !== 'PC' && extracted.type !== '개인PC') ? extracted.type : (extracted.상세용도 || '')).trim(); + } else { + oldVal = String((currentAsset as any)[f.key] || '').trim(); + newVal = String(extracted[f.key] || '').trim(); + } + + if (oldVal !== newVal) { + diffLogs.push(`${f.label}: ${oldVal || '(없음)'} → ${newVal || '(없음)'}`); + } + }); + } if (diffLogs.length > 0) { state.masterData.logs = state.masterData.logs || []; @@ -428,44 +424,31 @@ export function initHwModal(onSave: () => void, closeModalsCb: () => void) { id: Math.random().toString(36).substring(2, 9), assetId: currentAsset.id, date: new Date().toISOString().split('T')[0], - user: '관리자', + user: '담당자', details: diffLogs.join('\n') }); } } - // ---------------------------- const updated: any = { ...currentAsset, ...extracted, - IP주소: getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'), - 관리자: extracted.담당자_정, - 위치: isOpType ? extracted.보관위치 : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타') + [ASSET_SCHEMA.IP_ADDR.key]: getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'), + 위치: isOpType ? extracted[ASSET_SCHEMA.STORE_LOC.key] : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타') }; - // 현 사용조직 변경 시 이전 사용조직 자동 업데이트 - if (currentAsset.현사용조직 && currentAsset.현사용조직 !== extracted.현사용조직) { - updated.이전사용조직 = currentAsset.현사용조직; + if (currentAsset[ASSET_SCHEMA.ORG.key] && currentAsset[ASSET_SCHEMA.ORG.key] !== extracted[ASSET_SCHEMA.ORG.key]) { + updated[ASSET_SCHEMA.PREV_ORG.key] = currentAsset[ASSET_SCHEMA.ORG.key]; } - - // 비 PC 자산에 대해 상세유형(상세용도)을 유형과 동기화 - if (updated.type !== 'PC') { - updated.상세용도 = updated.type; - } - + if (updated.type !== 'PC') { updated.상세용도 = updated.type; } saveHardwareAsset(updated); onSave(); - setEditLock('hw-asset-form', 'view', { - saveBtnId: 'btn-save-hw-asset', - revertBtnId: 'btn-revert-hw-edit', - generateBtnId: 'btn-generate-hw-code', - addLogBtnId: 'btn-add-hw-log' - }); + setEditLock('hw-asset-form', 'view', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit', generateBtnId: 'btn-generate-hw-code', addLogBtnId: 'btn-add-hw-log' }); isEditMode = false; }); deleteBtn.addEventListener('click', () => { - if (currentAsset && confirm('정말로 삭제하시겠습니까?')) { + if (currentAsset && confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) { deleteHardwareAsset(currentAsset.id); onSave(); closeModalAction(); @@ -486,7 +469,7 @@ export function initHwModal(onSave: () => void, closeModalsCb: () => void) { const details = (document.getElementById('new-hw-log-details') as HTMLTextAreaElement).value; if (!date || !details) return; state.masterData.logs = state.masterData.logs || []; - state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: currentAsset.id, date, user: '관리자', details }); + state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: currentAsset.id, date, user: '담당자', details }); logModal.classList.add('hidden'); renderHwHistory(currentAsset.id); }); diff --git a/src/components/Modal/SWModal.ts b/src/components/Modal/SWModal.ts index 4e8e556..59c9449 100644 --- a/src/components/Modal/SWModal.ts +++ b/src/components/Modal/SWModal.ts @@ -2,8 +2,9 @@ import { state } from '../../core/state'; import { SoftwareAsset } from '../../core/excelHandler'; import { closeModals } from './BaseModal'; import { openSwUserModal } from './SWUserModal'; +import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { createIcons, History, Plus, X, Save, Edit2, RotateCcw } from 'lucide'; -import { CORP_LIST, TYPE_PREFIX_MAP } from './SharedData'; +import { CORP_LIST } from './SharedData'; import { generateOptionsHTML, setFieldValue, @@ -17,67 +18,63 @@ import { let currentSwAsset: SoftwareAsset | null = null; let isEditMode = false; +/** + * 소프트웨어 필드 매핑 (통합 스키마 기반) + * 소프트웨어는 자산번호를 사용하지 않으므로 제거함 + */ const SW_FIELD_MAP: Record = { - '법인': '법인', - '자산번호': '자산번호', - '제품명': '제품명', - '수량': '수량', - '금액': '금액', - '구매일': '구매일', - '납품업체': '납품업체', - '비고': '비고', - '플랫폼명': '플랫폼명', - '부서': '부서', - '계정명': '계정명', - '결제수단': '결제수단', - '연결카드번호': '연결카드번호', - '결제일': '결제일', - '당월청구액': '당월청구액', - '라이선스유형': '라이선스유형', - '만료일': '만료일', - '라이선스키': '라이선스키' + '법인': ASSET_SCHEMA.CORP.key, + '제품명': ASSET_SCHEMA.PRODUCT.key, + '수량': ASSET_SCHEMA.QTY.key, + '금액': ASSET_SCHEMA.PRICE.key, + '구매일': ASSET_SCHEMA.PURCHASE_YM.key, + '납품업체': ASSET_SCHEMA.VENDOR.key, + '비고': ASSET_SCHEMA.REMARKS.key, + '플랫폼명': ASSET_SCHEMA.PLATFORM.key, + '부서': '부서', + '계정명': ASSET_SCHEMA.ACCOUNT.key, + '결제수단': ASSET_SCHEMA.PAY_METHOD.key, + '연결카드번호': ASSET_SCHEMA.CARD_NUM.key, + '결제일': ASSET_SCHEMA.PAY_DAY.key, + '당월청구액': ASSET_SCHEMA.BILLING.key, + '라이선스유형': ASSET_SCHEMA.LICENSE_TYPE.key, + '만료일': ASSET_SCHEMA.EXPIRY.key, + '라이선스키': ASSET_SCHEMA.LICENSE_KEY.key }; const SW_FORM_HTML = `
기본 정보 (Identity)
- +
-
- -
- - -
-
- +
-
+
라이선스 및 계약 정보
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
관리 및 비고
-
-
-
-
+
+
+
+
@@ -149,7 +146,6 @@ export function openSwModal(asset: SoftwareAsset, mode: 'view' | 'add' = 'view') setEditLock('sw-asset-form', mode, { saveBtnId: 'btn-save-sw-asset', revertBtnId: 'btn-revert-sw-edit', - generateBtnId: 'btn-generate-sw-code', addLogBtnId: 'btn-add-sw-log' }); isEditMode = (mode === 'add'); @@ -171,9 +167,9 @@ export function initSwModal(onSave: () => void, closeModalsCb: () => void) { const logModalHTML = ` `; @@ -196,27 +192,12 @@ export function initSwModal(onSave: () => void, closeModalsCb: () => void) { setEditLock('sw-asset-form', 'view', { saveBtnId: 'btn-save-sw-asset', revertBtnId: 'btn-revert-sw-edit', - generateBtnId: 'btn-generate-sw-code', addLogBtnId: 'btn-add-sw-log' }); isEditMode = false; if (currentSwAsset) openSwModal(currentSwAsset, 'view'); }); - document.getElementById('btn-generate-sw-code')?.addEventListener('click', async () => { - const typeValue = getFieldValue('sw-asset-type'); - const purchaseDate = getFieldValue('sw-구매일'); - const typeCode = TYPE_PREFIX_MAP[typeValue] || 'SW'; - const dateStr = purchaseDate.replace(/[^0-9]/g, ''); - if (dateStr.length < 6) { alert('올바른 구매연월(YYYYMM)을 입력해주세요.'); return; } - const prefix = `${typeCode}-${dateStr.substring(0, 6)}-`; - try { - const res = await fetch(`http://172.16.40.100:3000/api/generate-asset-code?prefix=${prefix}`); - const data = await res.json(); - if (data.nextCode) setFieldValue('sw-자산번호', data.nextCode); - } catch (err) { alert('자산번호 생성에 실패했습니다.'); } - }); - // YYYYMM 입력 제한 로직 (숫자 6자리) document.getElementById('sw-구매일')?.addEventListener('input', (e) => { const target = e.target as HTMLInputElement; @@ -229,14 +210,13 @@ export function initSwModal(onSave: () => void, closeModalsCb: () => void) { setEditLock('sw-asset-form', 'edit', { saveBtnId: 'btn-save-sw-asset', revertBtnId: 'btn-revert-sw-edit', - generateBtnId: 'btn-generate-sw-code', - addLogBtnId: 'btn-add-sw-log' + addLogBtnId: 'btn-add-hw-log' }); isEditMode = true; return; } const extracted = autoExtractForm('sw', SW_FIELD_MAP); - const updated = { ...currentSwAsset, ...extracted, 수량: parseInt(extracted.수량 || '0') }; + const updated = { ...currentSwAsset, ...extracted, 수량: parseInt(extracted[ASSET_SCHEMA.QTY.key] || '0') }; let targetList: SoftwareAsset[] = []; if (updated.type === '구독SW') targetList = state.masterData.subSw; @@ -250,14 +230,13 @@ export function initSwModal(onSave: () => void, closeModalsCb: () => void) { setEditLock('sw-asset-form', 'view', { saveBtnId: 'btn-save-sw-asset', revertBtnId: 'btn-revert-sw-edit', - generateBtnId: 'btn-generate-sw-code', addLogBtnId: 'btn-add-sw-log' }); isEditMode = false; }); deleteBtn.addEventListener('click', () => { - if (currentSwAsset && confirm('삭제하시겠습니까?')) { + if (currentSwAsset && confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) { const type = currentSwAsset.type; if (type === '구독SW') state.masterData.subSw = state.masterData.subSw.filter(a => a.id !== currentSwAsset!.id); else if (type === '영구SW') state.masterData.permSw = state.masterData.permSw.filter(a => a.id !== currentSwAsset!.id); @@ -279,7 +258,7 @@ export function initSwModal(onSave: () => void, closeModalsCb: () => void) { const details = (document.getElementById('new-log-details') as HTMLTextAreaElement).value; if (!date || !details) return; state.masterData.logs = state.masterData.logs || []; - state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: currentSwAsset.id, date, user: '관리자', details }); + state.masterData.logs.push({ id: Math.random().toString(36).substring(2, 9), assetId: currentSwAsset.id, date, user: '담당자', details }); logModal.classList.add('hidden'); renderSwHistory(currentSwAsset.id); }); } diff --git a/src/components/Navigation.ts b/src/components/Navigation.ts index 598eae9..7cabde5 100644 --- a/src/components/Navigation.ts +++ b/src/components/Navigation.ts @@ -7,11 +7,11 @@ const MENU_CONFIG = { }, sw: { label: '소프트웨어', - tabs: ['대시보드', '구독SW', '영구SW', '클라우드'] + tabs: ['대시보드', '구독SW', '영구SW'] }, ops: { label: '운영 서비스', - tabs: ['대시보드', '서비스현황', '백업관리', '보안점검'] + tabs: ['도메인', '메일', '메신저', '청구비용'] } }; @@ -22,6 +22,7 @@ export function renderNavigation(onTabChange: (tab: string) => void) { const render = () => { navContainer.innerHTML = ''; + // 기존 메뉴 렌더링 (Object.keys(MENU_CONFIG) as Array).forEach(catKey => { const config = MENU_CONFIG[catKey]; const isActive = state.activeCategory === catKey; @@ -29,7 +30,6 @@ export function renderNavigation(onTabChange: (tab: string) => void) { const group = document.createElement('div'); group.className = `nav-group ${isActive ? 'active is-showing-shelf' : ''}`; - // 메인 카테고리 트리거 const trigger = document.createElement('div'); trigger.className = 'gnb-trigger'; trigger.textContent = config.label; @@ -45,7 +45,6 @@ export function renderNavigation(onTabChange: (tab: string) => void) { }); group.appendChild(trigger); - // 하위 탭 선반 (Shelf) const shelf = document.createElement('div'); shelf.className = 'lnb-shelf'; @@ -58,21 +57,34 @@ export function renderNavigation(onTabChange: (tab: string) => void) { e.stopPropagation(); state.activeCategory = catKey; state.activeSubTab = tab; - - if (btnAddAsset) { - btnAddAsset.classList.remove('hidden'); - } - + if (btnAddAsset) btnAddAsset.classList.remove('hidden'); render(); onTabChange(tab); }); shelf.appendChild(item); }); group.appendChild(shelf); - - // 마우스 오버 시 다른 그룹의 선반은 가리고 내 것만 보여주는 스타일은 CSS에서 처리함 navContainer.appendChild(group); }); + + // ─── '관리자' 메뉴 별도 추가 (GNB 스타일) ─── + const adminGroup = document.createElement('div'); + adminGroup.className = 'nav-group'; + + const adminTrigger = document.createElement('div'); + adminTrigger.className = 'gnb-trigger'; + adminTrigger.innerHTML = '관리자'; + adminTrigger.style.color = 'var(--text-muted)'; + adminTrigger.style.borderLeft = '1px solid var(--border-color)'; + adminTrigger.style.marginLeft = '1rem'; + adminTrigger.style.paddingLeft = '1.5rem'; + + adminTrigger.addEventListener('click', () => { + alert('준비중입니다.'); + }); + + adminGroup.appendChild(adminTrigger); + navContainer.appendChild(adminGroup); }; render(); diff --git a/src/core/schema.ts b/src/core/schema.ts new file mode 100644 index 0000000..a0e8f83 --- /dev/null +++ b/src/core/schema.ts @@ -0,0 +1,74 @@ +/** + * ITAM 통합 스키마 매퍼 (Unified Schema Mapper) + * + * key: 애플리케이션 내부 로직에서 사용하는 속성명 + * db: MySQL 데이터베이스 컬럼명 + * ui: 사용자에게 보여지는 UI 레이블 + */ + +export const ASSET_SCHEMA = { + // ─── 공통 필드 (Common) ─── + ID: { key: 'id', db: 'id', ui: 'ID' }, + TYPE: { key: 'type', db: 'type', ui: '자산유형' }, + CORP: { key: '법인', db: 'corp', ui: '구매법인' }, + ASSET_CODE: { key: '자산코드', db: 'asset_code', ui: '자산번호' }, + PURCHASE_YM: { key: '구매연월', db: 'purchase_date', ui: '구매연월' }, + ORG: { key: '현사용조직', db: 'current_org', ui: '현 사용조직' }, + PREV_ORG: { key: '이전사용조직', db: 'prev_org', ui: '이전 사용조직' }, + LOCATION: { key: '위치', db: 'location', ui: '설치위치' }, + MANAGER_MAIN: { key: '담당자_정', db: 'manager_main', ui: '담당자' }, + MANAGER_SUB: { key: '담당자_부', db: 'manager_sub', ui: '담당자(부)' }, + PRICE: { key: '금액', db: 'price', ui: '도입금액' }, + VENDOR: { key: '납품업체', db: 'vendor', ui: '납품업체' }, + DOC_NAME: { key: '품의서명', db: 'doc_name', ui: '품의서' }, + REMARKS: { key: '비고', db: 'remarks', ui: '비고' }, + + // ─── 하드웨어 상세 (Hardware) ─── + USER: { key: '사용자', db: 'purpose', ui: '사용자' }, + MODEL: { key: '모델명', db: 'model_name', ui: '모델명' }, + MAINBOARD: { key: '메인보드', db: 'mainboard', ui: '메인보드' }, + OS: { key: 'OS', db: 'os', ui: '운영체제' }, + CPU: { key: 'CPU', db: 'cpu', ui: 'CPU' }, + RAM: { key: 'RAM', db: 'ram', ui: 'RAM' }, + STORAGE1: { key: 'SSD1', db: 'storage1', ui: 'Storage 1' }, + STORAGE2: { key: 'SSD2', db: 'storage2', ui: 'Storage 2' }, + IP_ADDR: { key: 'IP주소', db: 'ip_address', ui: 'IP 주소 1' }, + IP_ADDR2: { key: 'IP2', db: 'ip2', ui: 'IP 주소 2' }, + MAC_ADDR: { key: 'MACaddress', db: 'mac_address', ui: 'MAC 주소' }, + STATUS: { key: '현재상태', db: 'status', ui: '현재상태' }, + STORE_LOC: { key: '보관위치', db: 'storage_location',ui: '보관위치' }, + + // ─── 소프트웨어/클라우드 상세 (SW/Cloud) ─── + PRODUCT: { key: '제품명', db: 'product_name', ui: '제품/서비스명' }, + PLATFORM: { key: '플랫폼명', db: 'platform_name', ui: '운영 플랫폼' }, + LICENSE_TYPE: { key: '라이선스유형', db: 'license_type', ui: '라이선스 유형' }, + LICENSE_KEY: { key: '라이선스키', db: 'license_key', ui: '라이선스 키' }, + QTY: { key: '수량', db: 'quantity', ui: '보유수량' }, + EXPIRY: { key: '만료일', db: 'expiry_date', ui: '만료/구독일' }, + ACCOUNT: { key: '계정명', db: 'account_name', ui: '계정(이메일)' }, + PAY_METHOD: { key: '결제수단', db: 'pay_method', ui: '결제수단' }, + PAY_DAY: { key: '결제일', db: 'pay_day', ui: '결제일' }, + CARD_NUM: { key: '연결카드번호', db: 'card_num', ui: '카드번호(뒷4자리)' }, + BILLING: { key: '당월청구액', db: 'monthly_fee', ui: '당월 청구액' } +}; + +/** + * 용어 사전 (UI 텍스트 전용) + */ +export const UI_TEXT = { + ACTION: { + ADD: '신규 등록', + EDIT: '수정', + SAVE: '저장', + DELETE: '삭제', + CANCEL: '취소', + CLOSE: '닫기', + HISTORY_ADD: '이력 추가', + RESET_FILTER: '필터 초기화' + }, + MESSAGES: { + CONFIRM_DELETE: '정말로 삭제하시겠습니까?', + SAVE_SUCCESS: '성공적으로 저장되었습니다.', + NO_DATA: '검색 결과가 없습니다.' + } +}; diff --git a/src/main.ts b/src/main.ts index 23586bc..506aaed 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,14 +2,14 @@ import { state, loadMasterDataFromDB } from './core/state'; import { renderNavigation } from './components/Navigation'; import { renderDashboard } from './views/DashboardView'; import { renderSWTable } from './views/SW_Table'; -import { downloadTemplate, exportToExcel, parseExcel, HardwareAsset, SoftwareAsset, SWUser } from './core/excelHandler'; +import { downloadTemplate, exportToExcel, parseExcel } from './core/excelHandler'; 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 { initDashboardDetailModal } from './components/Modal/DashboardDetailModal'; import { initGuide } from './components/Guide'; -import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw } from 'lucide'; +import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings } from 'lucide'; // --- DB 저장을 위한 세분화된 헬퍼 함수들 --- async function apiBatchSave(url: string, data: any[], label: string) { @@ -36,72 +36,50 @@ const savePermSwToDB = () => apiBatchSave('http://172.16.40.100:3000/api/sw/perm const saveCloudToDB = () => apiBatchSave('http://172.16.40.100:3000/api/cloud/batch', state.masterData.cloud, '클라우드'); const saveSwUsersToDB = () => apiBatchSave('http://172.16.40.100:3000/api/sw-users/batch', state.masterData.swUsers, 'SW사용자'); -// 모든 하드웨어 DB 동기화 async function saveAllHardwareToDB() { - await Promise.all([ - savePcToDB(), - saveServerToDB(), - saveStorageToDB(), - saveEquipToDB(), - saveMobileToDB() - ]); + await Promise.all([savePcToDB(), saveServerToDB(), saveStorageToDB(), saveEquipToDB(), saveMobileToDB()]); } -// 모든 소프트웨어 DB 동기화 async function saveAllSoftwareToDB() { - await Promise.all([ - saveSubSwToDB(), - savePermSwToDB(), - saveCloudToDB(), - saveSwUsersToDB() - ]); + await Promise.all([saveSubSwToDB(), savePermSwToDB(), saveCloudToDB(), saveSwUsersToDB()]); } // --- App Initialization --- function initApp() { - console.log('🚀 ITAM Dedicated System Initializing...'); const mainContent = document.getElementById('main-content')!; if (!mainContent) return; const { closeAllModals } = initBaseModal(); - + + // 탭 변경 시 실행될 통합 렌더링 함수 + const handleTabChange = (tab: string) => { + if (tab === '대시보드') { + renderDashboard(mainContent); + } else { + renderSWTable(mainContent); + } + }; + try { - renderNavigation((tab) => { - if (tab === '대시보드') { - renderDashboard(mainContent); - } else { - renderSWTable(mainContent); + // 1. 네비게이션 렌더링 및 콜백 연결 + renderNavigation(handleTabChange); + + // 2. 각종 모달 및 가이드 초기화 + initHwModal(() => { saveAllHardwareToDB(); renderSWTable(mainContent); }, closeAllModals); + initSwModal(() => { saveAllSoftwareToDB(); renderSWTable(mainContent); }, closeAllModals); + initSwUserModal(() => { saveSwUsersToDB(); renderSWTable(mainContent); }, closeAllModals); + initDashboardDetailModal(); + initGuide(); + + // 4. DB 데이터 로드 및 초기 화면 렌더링 + loadMasterDataFromDB().then((success) => { + if (success) { + handleTabChange(state.activeSubTab); } }); - // 모달 초기화 - initHwModal(() => { saveAllHardwareToDB(); renderSWTable(mainContent); }, closeAllModals); - - initSwModal(() => { - saveAllSoftwareToDB(); - renderSWTable(mainContent); - }, closeAllModals); - - initSwUserModal(() => { - saveSwUsersToDB(); - renderSWTable(mainContent); - }, closeAllModals); - - initDashboardDetailModal(); - initGuide(); // 가이드 초기화 추가 } catch (e) { console.error('❌ Initialization failed:', e); } - // 초기 로드 시 대시보드 렌더링 - renderDashboard(mainContent); - - // DB에서 데이터 로드 후 화면 갱신 - loadMasterDataFromDB().then((success) => { - if (success) { - if (state.activeSubTab === '대시보드') renderDashboard(mainContent); - else renderSWTable(mainContent); - } - }); - // 버튼 이벤트 바인딩 document.getElementById('btn-download-template')?.addEventListener('click', () => downloadTemplate()); document.getElementById('btn-export-excel')?.addEventListener('click', () => exportToExcel(state.masterData)); @@ -112,46 +90,24 @@ function initApp() { if (file) { const data = await parseExcel(file); state.masterData = data; - await Promise.all([ - saveAllHardwareToDB(), - saveAllSoftwareToDB() - ]); - renderSWTable(mainContent); + await Promise.all([saveAllHardwareToDB(), saveAllSoftwareToDB()]); + handleTabChange(state.activeSubTab); } }); document.getElementById('btn-add-asset')?.addEventListener('click', () => { const tab = state.activeSubTab; const cat = state.activeCategory; - if (cat === 'hw') { - // 탭 명칭을 실제 유형명으로 매핑 - let defaultType = ''; - if (tab === '개인PC') defaultType = 'PC'; - else if (tab === '서버') defaultType = '서버'; - else if (tab === '스토리지') defaultType = '스토리지'; - else if (tab === '전산비품') defaultType = 'CPU'; - else if (tab === '모바일기기') defaultType = '모바일'; - - openHwModal({ - id: Math.random().toString(36).substring(2, 9), - type: defaultType, - 법인: '한맥', 자산코드: '', 명칭: '', 설치위치: '', MACaddress: '', HW사양: '', OS: '', 연락처: '', 담당부서: '' - } as any, 'add'); + let defaultType = (tab === '개인PC') ? 'PC' : (tab === '서버' ? '서버' : (tab === '스토리지' ? '스토리지' : (tab === '전산비품' ? 'CPU' : '모바일'))); + openHwModal({ id: Math.random().toString(36).substring(2, 9), type: defaultType, 법인: '한맥', 자산코드: '', 명칭: '', 설치위치: '', MACaddress: '', HW사양: '', OS: '', 연락처: '', 담당부서: '' } as any, 'add'); } else if (cat === 'sw') { - // 소프트웨어 대시보드 또는 개별 탭에서 추가 - let defaultType = tab; - if (tab === '대시보드') defaultType = '구독SW'; // SW는 기본 레이아웃을 위해 하나 지정하되 필드는 빈값 - - openSwModal({ - id: Math.random().toString(36).substring(2, 9), - type: defaultType, 제품명: '', 금액: '', 수량: 1, 계정명: '', 납품업체: '', 비고: '', 법인: '한맥' - } as any, 'add'); + openSwModal({ id: Math.random().toString(36).substring(2, 9), type: tab === '대시보드' ? '구독SW' : tab, 제품명: '', 금액: '', 수량: 1, 계정명: '', 납품업체: '', 비고: '', 법인: '한맥' } as any, 'add'); } }); createIcons({ - icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen } + icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History, RefreshCcw, BookOpen, Settings } }); } diff --git a/src/styles/common.css b/src/styles/common.css index 01ce326..56ec9af 100644 --- a/src/styles/common.css +++ b/src/styles/common.css @@ -1,7 +1,67 @@ :root { - --primary-color: #1E5149; - --primary-hover: #153c36; - --primary-light: #edf2f1; + /* --- System Colors (Added) --- */ + --color-red: #F21D0D; + --color-pink: #E8175E; + --color-magenta: #B92ED1; + --color-purple: #6D3DC2; + --color-navy: #4255bd; + --color-blue: #0D8DF2; + --color-cyan: #03AEFC; + --color-green: #4DB251; + --color-yellow: #FFBF00; + --color-orange: #FF9800; + --color-dahong: #FF3D00; + --color-brown: #A0705F; + --color-iron: #7F7F7F; + --color-steel: #688897; + + --color-red-light: #FEE9E7; + --color-pink-light: #FDE8EF; + --color-magenta-light: #F8EBFB; + --color-purple-light: #F1ECF9; + --color-navy-light: #EDEEF9; + --color-blue-light: #E7F4FE; + --color-cyan-light: #E6F7FF; + --color-green-light: #EEF8EE; + --color-yellow-light: #FFF9E6; + --color-orange-light: #FFF5E6; + --color-dahong-light: #FFECE6; + --color-brown-light: #F6F1EF; + --color-iron-light: #F3F3F3; + --color-steel-light: #F0F4F5; + + --color-red-medium: #FAA59E; + --color-pink-medium: #F6A2BF; + --color-magenta-medium: #E3ABEC; + --color-purple-medium: #C5B1E7; + --color-navy-medium: #B3BBE5; + --color-blue-medium: #9ED1FA; + --color-cyan-medium: #9ADFFE; + --color-green-medium: #B8E0B9; + --color-yellow-medium: #FFE599; + --color-orange-medium: #FFD699; + --color-dahong-medium: #FFB199; + --color-brown-medium: #D9C6BF; + --color-iron-medium: #CCCCCC; + --color-steel-medium: #C3CFD5; + + /* --- Primary Brand Levels --- */ + --primary-lv-0: #E9EEED; + --primary-lv-1: #D2DCDB; + --primary-lv-2: #A5B9B6; + --primary-lv-3: #789792; + --primary-lv-4: #4B746D; + --primary-lv-5: #35635C; + --primary-lv-6: #1E5149; + --primary-lv-7: #1B443D; + --primary-lv-8: #193833; + --primary-lv-9: #162A27; + + /* --- Legacy Aliases (Maintained for compatibility) --- */ + --primary-color: var(--primary-lv-6); + --primary-hover: var(--primary-lv-5); + --primary-light: var(--primary-lv-0); + --text-main: #111827; --text-muted: #6B7280; --border-color: #E5E7EB; @@ -9,7 +69,7 @@ --bg-light: #FAFAFA; --sidebar-bg: #ffffff; --white: #FFFFFF; - --danger: #dc2626; + --danger: var(--color-red); --dash-primary: #6cc020; --dash-light: #f2f9ec; @@ -22,14 +82,15 @@ box-sizing: border-box; margin: 0; padding: 0; + letter-spacing: -0.02em; + /* 모든 요소에 자간 규칙 일괄 적용 */ } body { - font-family: 'Pretendard Variable', Pretendard, sans-serif; + font-family: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; color: var(--text-main); background-color: var(--bg-color); line-height: 1.5; - letter-spacing: -0.02em; font-size: 14px; overflow: hidden; } @@ -57,14 +118,32 @@ body { gap: 1.5rem; } +.brand { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.main-logo { + height: 34px; + width: auto; +} + .brand h1 { - font-size: 1.2rem; + font-size: 1.1rem; + /* 전체적으로 살짝 축소 */ font-weight: 800; color: var(--text-main); white-space: nowrap; - margin-right: 1rem; } -.brand h1 span { color: var(--primary-color); } + +.brand h1 .sub-title { + font-size: 0.85rem; + /* 영문 제목은 더 작게 */ + color: var(--primary-color); + font-weight: 600; + margin-left: 0.25rem; +} .integrated-nav { flex: 1; @@ -93,7 +172,7 @@ body { } .lnb-shelf { - display: none; + display: none; align-items: center; gap: 0.25rem; padding: 0 0.75rem; @@ -118,7 +197,11 @@ body { white-space: nowrap; } -.lnb-item:hover { color: var(--primary-color); background-color: var(--bg-color); } +.lnb-item:hover { + color: var(--primary-color); + background-color: var(--bg-color); +} + .lnb-item.active { color: var(--primary-color); background-color: var(--primary-light); @@ -126,12 +209,23 @@ body { } @keyframes fadeIn { - from { opacity: 0; transform: translateX(-5px); } - to { opacity: 1; transform: translateX(0); } + from { + opacity: 0; + transform: translateX(-5px); + } + + to { + opacity: 1; + transform: translateX(0); + } } /* --- Global Actions & Buttons --- */ -.header-actions { display: flex; gap: 0.3rem; align-items: center; } +.header-actions { + display: flex; + gap: 0.3rem; + align-items: center; +} .btn { display: inline-flex; @@ -145,30 +239,87 @@ body { cursor: pointer; height: 28px; line-height: 1; + white-space: nowrap; /* 텍스트 줄바꿈 방지 */ + flex-shrink: 0; /* 크기 찌그러짐 방지 */ } -.btn i, .btn svg { width: 12px !important; height: 12px !important; } +.btn i, +.btn svg { + width: 12px !important; + height: 12px !important; +} -.btn-primary { background-color: var(--primary-color); color: var(--white); border: 1px solid var(--primary-color); } -.btn-outline { background-color: transparent; color: var(--text-muted); border: 1px solid var(--border-color); } -.btn-danger { color: var(--danger) !important; border-color: var(--danger) !important; } +.btn-primary { + background-color: var(--primary-color); + color: var(--white); + border: 1px solid var(--primary-color); +} + +.btn-outline { + background-color: transparent; + color: var(--text-muted); + border: 1px solid var(--border-color); +} + +.btn-danger { + color: var(--danger) !important; + border-color: var(--danger) !important; +} /* --- Layout Frame --- */ .content-area { flex: 1; - padding: 2rem; - overflow-y: auto; + padding: 0 2rem; + /* 좌우 여백만 유지 */ + overflow: hidden; + /* 전체 스크롤 차단 */ + display: flex; + flex-direction: column; } .view-container { + flex: 1; width: 100%; display: flex; flex-direction: column; - gap: 1.5rem; + overflow: hidden; + /* 내부 스크롤을 유도하기 위해 설정 */ } -.hidden { display: none !important; } -.text-nowrap { white-space: nowrap; } +/* --- Footer --- */ +.main-footer { + height: 40px; + background-color: var(--white); + border-top: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0 1.5rem; + flex-shrink: 0; +} + +.main-footer p { + font-family: 'Pretendard Variable', Pretendard, sans-serif; + font-size: 0.75rem; + font-weight: 300; + line-height: 1.25rem; + letter-spacing: -0.0175rem; + color: #777777; + user-select: none; + pointer-events: all; + -webkit-user-drag: none; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.hidden { + display: none !important; +} + +.text-nowrap { + white-space: nowrap; +} /* --- Utility Styles --- */ .badge { @@ -178,8 +329,16 @@ body { font-weight: 700; white-space: nowrap; } -.badge-primary { background-color: var(--primary-color); color: white; } -.badge-muted { background-color: #9CA3AF; color: white; } + +.badge-primary { + background-color: var(--primary-color); + color: white; +} + +.badge-muted { + background-color: #9CA3AF; + color: white; +} .text-tag { color: var(--text-muted); @@ -190,4 +349,27 @@ body { background-color: var(--bg-light); } -.font-bold { font-weight: 700; } +.font-bold { + font-weight: 700; +} + +/* --- Responsive Design (Tablet & Mobile) --- */ +@media (max-width: 1200px) { + .header-container { gap: 0.75rem; padding: 0 1rem; } + .brand h1 { font-size: 1rem; } + .brand h1 .sub-title { font-size: 0.75rem; } +} + +@media (max-width: 992px) { + .main-header { height: auto; padding: 0.5rem 0; } + .header-container { flex-direction: column; align-items: flex-start; gap: 0.5rem; } + .integrated-nav { width: 100%; justify-content: flex-start; border-top: 1px solid var(--border-color); padding-top: 0.5rem; } + .header-actions { width: 100%; justify-content: flex-end; padding-top: 0.5rem; } + .content-area { padding: 0 1rem; } +} + +@media (max-width: 768px) { + .brand h1 .sub-title { display: none; } /* 아주 좁은 화면에선 영문명 숨김 */ + .header-actions .btn span { display: none; } /* 버튼 텍스트 숨기고 아이콘만 표시 */ + .header-actions .btn { padding: 0 0.5rem; } +} \ No newline at end of file diff --git a/src/styles/guide.css b/src/styles/guide.css index e2d40cf..73b0e13 100644 --- a/src/styles/guide.css +++ b/src/styles/guide.css @@ -1,112 +1,24 @@ -/* ITAM Guide Modal Styles */ -:root { - --guide-modal-width: 1060px; - --guide-modal-height: 92vh; - --guide-primary: #1E5149; - --guide-accent: #6cc020; -} +/* ITAM Guide Modal Styles - Updated to match common modal style */ -/* Floating Trigger Button - REMOVED (now in header) */ -.guide-trigger { - display: none; -} - -/* Modal Overlay */ -.guide-overlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background-color: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(4px); - z-index: 2000; - opacity: 0; - visibility: hidden; - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; -} - -.guide-overlay.active { - opacity: 1; - visibility: visible; -} - -/* Guide Modal */ -.guide-modal { - width: var(--guide-modal-width); - max-width: 94vw; - height: var(--guide-modal-height); - background-color: #ffffff; - border-radius: 14px; - overflow: hidden; - box-shadow: 0 24px 60px rgba(0,0,0,0.3); - display: flex; - flex-direction: column; - transform: translateY(20px) scale(0.97); - opacity: 0; - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); -} - -.guide-overlay.active .guide-modal { - transform: translateY(0) scale(1); - opacity: 1; -} - -/* Header */ -.guide-header { - padding: 1.1rem 1.5rem; +/* Tab Container (below header) */ +.guide-tabs-container { + background: #FAFAFA; border-bottom: 1px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; - background: linear-gradient(135deg, var(--guide-primary), #2a6d63); - color: white; - flex-shrink: 0; -} - -.guide-header h2 { - font-size: 1.15rem; - font-weight: 700; - display: flex; - align-items: center; - gap: 10px; - margin: 0; -} - -.btn-close-guide { - background: rgba(255, 255, 255, 0.12); - border: none; - color: white; - cursor: pointer; - width: 30px; - height: 30px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.2s; -} - -.btn-close-guide:hover { - background: rgba(255, 255, 255, 0.3); -} - -/* ===== Tab Navigation ===== */ -.guide-tabs { - display: flex; - border-bottom: 1px solid var(--border-color); - background: #f8faf9; padding: 0 1.5rem; flex-shrink: 0; - gap: 2px; - overflow-x: auto; } +.guide-tabs { + display: flex; + gap: 2px; + overflow-x: auto; + scrollbar-width: none; +} + +.guide-tabs::-webkit-scrollbar { display: none; } + .guide-tab { - padding: 0.7rem 1rem; + padding: 0.75rem 1.25rem; font-size: 13px; font-weight: 600; color: var(--text-muted); @@ -114,37 +26,27 @@ border-bottom: 2px solid transparent; transition: all 0.2s ease; white-space: nowrap; - position: relative; - top: 1px; } .guide-tab:hover { - color: var(--guide-primary); + color: var(--primary-color); background: rgba(30, 81, 73, 0.04); } .guide-tab.active { - color: var(--guide-primary); - border-bottom-color: var(--guide-primary); + color: var(--primary-color); + border-bottom-color: var(--primary-color); background: white; } -/* ===== Content Area ===== */ +/* Content Area */ .guide-body { - flex: 1; - overflow-y: auto; - padding: 0; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE/Edge */ -} - -.guide-body::-webkit-scrollbar { - display: none; /* Chrome/Safari */ + padding-bottom: 2rem; } .guide-tab-panel { display: none; - padding: 1.5rem 2rem 2rem; + padding: 1.5rem 0; animation: guideFadeIn 0.3s ease; } @@ -157,12 +59,12 @@ to { opacity: 1; transform: translateY(0); } } -/* ===== Section Styles ===== */ +/* Section Styles */ .guide-section { display: flex; flex-direction: column; gap: 0.75rem; - margin-bottom: 1.5rem; + margin-bottom: 2rem; } .guide-section:last-child { @@ -171,84 +73,66 @@ .guide-section h3 { font-size: 1rem; - padding-bottom: 0.4rem; - border-bottom: 2px solid var(--guide-primary); - color: var(--guide-primary); + 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-section h4 { - font-size: 0.9rem; - color: var(--text-main); - margin: 0.6rem 0 0.2rem; - font-weight: 700; -} - .guide-text { font-size: 13px; - color: var(--text-muted); + color: var(--text-main); line-height: 1.7; margin: 0; } -.guide-text strong { - color: var(--text-main); -} - -/* ===== Flowchart ===== */ +/* Flowchart Styles */ .flow-container { display: flex; flex-direction: column; align-items: center; - gap: 0.5rem; - padding: 1.25rem; - background-color: #f8faf9; - border-radius: 12px; - border: 1px dashed #d0d7d5; + 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: 0.75rem; - align-items: stretch; + gap: 1rem; + align-items: center; } .flow-step { flex: 1; background: white; - padding: 0.65rem 0.9rem; - border-radius: 8px; + padding: 1rem; + border-radius: 6px; border: 1px solid var(--border-color); display: flex; align-items: flex-start; - gap: 10px; - transition: transform 0.2s, box-shadow 0.2s; -} - -.flow-step:hover { - transform: translateY(-2px); - box-shadow: 0 4px 14px rgba(0,0,0,0.06); - border-color: var(--guide-primary); + gap: 12px; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); } .flow-step .step-number { - width: 22px; - height: 22px; - min-width: 22px; + width: 24px; + height: 24px; + min-width: 24px; border-radius: 50%; - background-color: var(--guide-primary); + background-color: var(--primary-color); color: white; - font-size: 11px; + font-size: 12px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; - margin-top: 1px; } .flow-step .step-label { @@ -259,91 +143,46 @@ } .flow-step .step-desc { - font-size: 11.5px; + font-size: 12px; color: var(--text-muted); line-height: 1.5; - margin-top: 2px; -} - -.flow-arrow { - color: #b5c4c0; - width: 16px !important; - height: 16px !important; + margin-top: 4px; } .flow-arrow-right { - color: #b5c4c0; - width: 16px !important; - height: 16px !important; + color: var(--text-muted); display: flex; align-items: center; - flex-shrink: 0; } -/* ===== Info Table ===== */ +/* Info Table Style */ .guide-info-table { width: 100%; border-collapse: collapse; - font-size: 12.5px; - margin-top: 0.5rem; + font-size: 13px; } .guide-info-table th { - background: #f0f4f3; - color: var(--guide-primary); + background: #f8faf9; + color: var(--primary-color); font-weight: 700; - padding: 0.5rem 0.75rem; + padding: 0.75rem; text-align: left; - border-bottom: 2px solid var(--guide-primary); + border-bottom: 1px solid var(--border-color); } .guide-info-table td { - padding: 0.45rem 0.75rem; - border-bottom: 1px solid var(--border-color); + padding: 0.75rem; + border-bottom: 1px solid #f3f4f6; color: var(--text-main); - line-height: 1.5; } -.guide-info-table tr:hover td { - background: #f8faf9; -} - -/* ===== Tip Box ===== */ +/* Tip Box Style */ .guide-tip { - background: linear-gradient(135deg, #f0f9eb, #e8f5e0); - border-left: 4px solid var(--guide-accent); - border-radius: 0 8px 8px 0; - padding: 0.75rem 1rem; - font-size: 12.5px; - color: #2d5016; + background: var(--primary-light); + border-left: 4px solid var(--primary-color); + padding: 1rem; + font-size: 13px; + color: var(--primary-color); line-height: 1.6; } - -.guide-tip strong { - color: #1a3a0a; -} - -/* ===== Warning Box ===== */ -.guide-warn { - background: linear-gradient(135deg, #fff8ed, #fff3e0); - border-left: 4px solid #ff9800; - border-radius: 0 8px 8px 0; - padding: 0.75rem 1rem; - font-size: 12.5px; - color: #7a4a00; - line-height: 1.6; -} - -/* ===== Badge ===== */ -.guide-badge { - display: inline-block; - padding: 2px 8px; - border-radius: 4px; - font-size: 11px; - font-weight: 700; -} - -.guide-badge.green { background: #e6f4ea; color: #137333; } -.guide-badge.orange { background: #fff4e5; color: #b45309; } -.guide-badge.blue { background: #e8f0fe; color: #1a56db; } -.guide-badge.red { background: #fce8e6; color: #c5221f; } diff --git a/src/styles/table.css b/src/styles/table.css index a18f087..357d9ec 100644 --- a/src/styles/table.css +++ b/src/styles/table.css @@ -4,11 +4,10 @@ display: flex; flex-wrap: wrap; gap: 1.25rem; - background-color: var(--white); - padding: 1.5rem; - border: 1px solid var(--border-color); - border-radius: 8px; + padding: 1.5rem 0; /* 좌우 패딩 제거, 상하 여백 유지 */ + border-bottom: 1px solid var(--border-color); /* 하단 구분선만 남김 */ align-items: flex-end; + margin-bottom: 0.5rem; } .search-item { @@ -23,7 +22,7 @@ .search-item label { font-size: 11px; - font-weight: 800; + font-weight: 700; color: var(--text-muted); } @@ -35,70 +34,92 @@ border-radius: 4px; font-size: 14px; outline: none; + background-color: var(--white); } +/* 셀렉트 박스 화살표 여백 절대 고정 (수정 금지) */ .search-item select { - padding-right: 2.5rem; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-9'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.75rem center; + padding-right: 2.5rem !important; + cursor: pointer; } +.search-item input:focus, +.search-item select:focus { + border-color: var(--primary-color); +} + +/* 필터 초기화 버튼 크기 조정 (입력창 높이 38px에 맞춤) */ .btn-reset { + margin-left: auto; height: 38px !important; - padding: 0 0.8rem !important; - font-size: 12px !important; - display: inline-flex !important; - align-items: center !important; - gap: 0.35rem !important; - border-radius: 4px !important; + color: var(--text-muted) !important; + padding: 0 1.2rem !important; + display: inline-flex; + align-items: center; + justify-content: center; } .table-container { + flex: 1; background-color: var(--white); border-top: 1px solid var(--border-color); - border-bottom: 1px solid var(--border-color); - border-left: none; - border-right: none; overflow: auto; - max-height: calc(100vh - 240px); } table { width: 100%; border-collapse: collapse; + table-layout: auto; } th, td { - padding: 1rem 1.5rem; - border-bottom: 1px solid var(--border-color); - text-align: left; + padding: 0.8rem 1.2rem; + border-bottom: 1px solid #F3F4F6; + text-align: left; /* 기본은 좌측 정렬 */ white-space: nowrap; } th { background-color: #FAFAFA; - font-weight: 700; + font-size: 13px; + font-weight: 600; color: var(--text-muted); - font-size: 12px; position: sticky; top: 0; z-index: 10; box-shadow: inset 0 -1px 0 var(--border-color); - text-transform: uppercase; + text-transform: none; } td { - font-size: 14px; + font-size: 13px; + color: var(--text-main); + font-weight: 400; } tbody tr:hover { background-color: #F9FAFB; } -.btn-sm { - padding: 0.25rem 0.5rem; - font-size: 11px; - height: 24px; +/* 정렬 클래스 강제 적용 */ +.text-center { text-align: center !important; } +.text-right { text-align: right !important; } +.text-left { text-align: left !important; } + +.btn-icon { + padding: 0.25rem; + border: none; + background: none; + cursor: pointer; + color: var(--text-muted); + transition: color 0.2s; +} + +.btn-icon:hover { + color: var(--primary-color); +} + +.btn-icon svg { + width: 16px; + height: 16px; } diff --git a/src/views/List/CloudListView.ts b/src/views/List/CloudListView.ts index 8efdc1f..ce1c30e 100644 --- a/src/views/List/CloudListView.ts +++ b/src/views/List/CloudListView.ts @@ -1,20 +1,24 @@ import { state } from '../../core/state'; import { openSwModal } from '../../components/Modal/SWModal'; -import { createIcons, Cloud, CreditCard, DollarSign } from 'lucide'; +import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; +import { createIcons, Cloud, CreditCard, DollarSign, RefreshCcw } from 'lucide'; +/** + * 클라우드(운영 서비스) 자산 목록 뷰 + * 라인 정렬 보정 및 헤더 통일 + */ export function renderCloudList(container: HTMLElement) { - // DB에서 직접 로드된 전용 배열을 사용하여 데이터 소스를 일원화함 const getFullList = () => state.masterData.cloud || []; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; filterBar.innerHTML = `
- +
- +
`; container.appendChild(filterBar); @@ -33,16 +37,16 @@ export function renderCloudList(container: HTMLElement) { table.innerHTML = ` - No. - 플랫폼명 - 법인 - 담당부서 - 진행 프로젝트(사용용도) - 계정명(관리자) - 결제수단 - 결제일 - 당월 청구액 - 비고 + No. + ${ASSET_SCHEMA.PLATFORM.ui} + ${ASSET_SCHEMA.CORP.ui} + 담당부서 + 용도(프로젝트) + ${ASSET_SCHEMA.ACCOUNT.ui} + ${ASSET_SCHEMA.PAY_METHOD.ui} + ${ASSET_SCHEMA.PAY_DAY.ui} + ${ASSET_SCHEMA.BILLING.ui} + ${ASSET_SCHEMA.REMARKS.ui} @@ -61,16 +65,16 @@ export function renderCloudList(container: HTMLElement) { const filtered = getFullList().filter(asset => { const kwMatch = !keyword || - (asset.제품명 || '').toLowerCase().includes(keyword) || + (asset[ASSET_SCHEMA.PRODUCT.key] || '').toLowerCase().includes(keyword) || (asset.부서 || '').toLowerCase().includes(keyword) || - (asset.계정명 || '').toLowerCase().includes(keyword); - const payMatch = !payment || asset.결제수단 === payment; + (asset[ASSET_SCHEMA.ACCOUNT.key] || '').toLowerCase().includes(keyword); + const payMatch = !payment || asset[ASSET_SCHEMA.PAY_METHOD.key] === payment; return kwMatch && payMatch; }); tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = '등록된 클라우드 서비스가 없습니다.'; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } @@ -78,29 +82,30 @@ export function renderCloudList(container: HTMLElement) { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; - const paymentBadge = asset.결제수단 === '법인카드' - ? '법인카드 (' + (asset.연결카드번호||'미상') + ')' - : (asset.결제수단 === '인보이스' + const payMethod = asset[ASSET_SCHEMA.PAY_METHOD.key]; + const paymentBadge = payMethod === '법인카드' + ? `법인카드` + : (payMethod === '인보이스' ? '인보이스' : '미설정'); tr.innerHTML = ` - ${idx+1} - ${asset.플랫폼명||'미지정'} - ${asset.법인||''} - ${asset.부서||''} - ${asset.제품명||''} - ${asset.계정명||''} - ${paymentBadge} - ${asset.결제일 ? asset.결제일 + '일' : ''} - ₩ ${asset.당월청구액 ? Number(asset.당월청구액).toLocaleString() : '0'} - ${asset.비고||''} + ${idx+1} + ${asset[ASSET_SCHEMA.PLATFORM.key]||'미지정'} + ${asset[ASSET_SCHEMA.CORP.key]||''} + ${asset.부서||''} + ${asset[ASSET_SCHEMA.PRODUCT.key]||''} + ${asset[ASSET_SCHEMA.ACCOUNT.key]||''} + ${paymentBadge} + ${asset[ASSET_SCHEMA.PAY_DAY.key] ? asset[ASSET_SCHEMA.PAY_DAY.key] + '일' : ''} + ₩ ${asset[ASSET_SCHEMA.BILLING.key] ? Number(asset[ASSET_SCHEMA.BILLING.key]).toLocaleString() : '0'} + ${asset[ASSET_SCHEMA.REMARKS.key]||''} `; tr.addEventListener('click', () => openSwModal(asset, 'view')); tbody.appendChild(tr); }); - createIcons({ icons: { Cloud, CreditCard, DollarSign } }); + createIcons({ icons: { Cloud, CreditCard, DollarSign, RefreshCcw } }); }; document.getElementById('filter-keyword')?.addEventListener('input', updateTable); diff --git a/src/views/List/EquipmentListView.ts b/src/views/List/EquipmentListView.ts index 37bd4b0..f8c8f6e 100644 --- a/src/views/List/EquipmentListView.ts +++ b/src/views/List/EquipmentListView.ts @@ -1,29 +1,31 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, sortAssets } from '../../core/utils'; +import { formatInline, createBadge, sortAssets } from '../../core/utils'; +import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { createIcons, RefreshCcw } from 'lucide'; +/** + * 전산비품 자산 목록 뷰 + * 라인 정렬 보정 및 헤더 통일 + */ export function renderEquipmentList(container: HTMLElement) { const fullList = sortAssets(state.masterData.equip); const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - const corps = Array.from(new Set(fullList.map(a => a.법인))).filter(Boolean).sort(); + const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CORP.key]))).filter(Boolean).sort(); filterBar.innerHTML = `
- +
- +
- `; container.appendChild(filterBar); @@ -34,16 +36,16 @@ export function renderEquipmentList(container: HTMLElement) { table.innerHTML = ` - No. - 상태 - 구매법인 - 유형 - 자산번호 - 모델명 - 보관위치 - 관리자 - 구매연월 - 금액 + No. + ${ASSET_SCHEMA.STATUS.ui} + ${ASSET_SCHEMA.CORP.ui} + 유형 + ${ASSET_SCHEMA.ASSET_CODE.ui} + ${ASSET_SCHEMA.MODEL.ui} + ${ASSET_SCHEMA.STORE_LOC.ui} + 담당자(정/부) + ${ASSET_SCHEMA.PURCHASE_YM.ui} + ${ASSET_SCHEMA.PRICE.ui} @@ -61,14 +63,17 @@ export function renderEquipmentList(container: HTMLElement) { const corp = corpSelect ? corpSelect.value : ''; const filtered = fullList.filter(asset => { - const matchKeyword = !keyword || String(asset.자산코드||'').toLowerCase().includes(keyword) || String(asset.모델명||'').toLowerCase().includes(keyword) || String(asset.현사용조직||'').toLowerCase().includes(keyword); - const matchCorp = !corp || asset.법인 === corp; + const matchKeyword = !keyword || + String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) || + String(asset[ASSET_SCHEMA.MODEL.key]||'').toLowerCase().includes(keyword) || + String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword); + const matchCorp = !corp || asset[ASSET_SCHEMA.CORP.key] === corp; return matchKeyword && matchCorp; }); tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `검색 결과가 없습니다.`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } @@ -76,32 +81,34 @@ export function renderEquipmentList(container: HTMLElement) { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; - const statusColors: Record = { - '대여중': '#3b82f6', - '보관중': '#1E5149', - '수리중': '#ef4444', - '기타': '#6b7280' - }; - const statusColor = statusColors[asset.현재상태 || '보관중'] || '#6b7280'; - const statusBadge = `${asset.현재상태 || '보관중'}`; + const statusColors: Record = { '대여중': 'primary', '보관중': 'success', '수리중': 'danger', '기타': 'muted' }; + const statusValue = asset[ASSET_SCHEMA.STATUS.key] || '보관중'; + const statusType = statusColors[statusValue] || 'muted'; + const statusBadge = `${statusValue}`; + + const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || ''; + const subManager = asset[ASSET_SCHEMA.MANAGER_SUB.key] || ''; + const managerHtml = [ + mainManager ? `${createBadge('정', 'primary')} ${mainManager}` : '', + subManager ? `${createBadge('부', 'muted')} ${subManager}` : '' + ].filter(v => v !== '').join(' / '); tr.innerHTML = ` - ${idx + 1} - ${statusBadge} - ${asset.법인} - ${asset.type} - ${asset.자산코드 || '-'} - ${formatInline(asset.모델명 || asset.명칭)} - ${asset.보관위치 || '-'} - ${formatInline(asset.담당자_정 || asset.관리자)} - ${asset.구매일 || ''} - ${asset.금액 || '0'} + ${idx + 1} + ${statusBadge} + ${asset[ASSET_SCHEMA.CORP.key]} + ${asset[ASSET_SCHEMA.TYPE.key]} + ${asset[ASSET_SCHEMA.ASSET_CODE.key] || '-'} + ${formatInline(asset[ASSET_SCHEMA.MODEL.key] || asset.명칭)} + ${asset[ASSET_SCHEMA.STORE_LOC.key] || '-'} + ${managerHtml || '-'} + ${asset[ASSET_SCHEMA.PURCHASE_YM.key] || ''} + ${Number(asset[ASSET_SCHEMA.PRICE.key]||0).toLocaleString()} `; - tr.addEventListener('click', (e) => { - if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); - }); + tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); }); + createIcons({ icons: { RefreshCcw } }); }; document.getElementById('filter-keyword')?.addEventListener('input', updateTable); diff --git a/src/views/List/MobileListView.ts b/src/views/List/MobileListView.ts index 4ab2bac..8a9c7d5 100644 --- a/src/views/List/MobileListView.ts +++ b/src/views/List/MobileListView.ts @@ -1,29 +1,31 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, sortAssets } from '../../core/utils'; +import { formatInline, createBadge, sortAssets } from '../../core/utils'; +import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { createIcons, RefreshCcw } from 'lucide'; +/** + * 모바일기기 자산 목록 뷰 + * 라인 정렬 보정 및 헤더 통일 + */ export function renderMobileList(container: HTMLElement) { const fullList = sortAssets(state.masterData.mobile); const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - const corps = Array.from(new Set(fullList.map(a => a.법인))).filter(Boolean).sort(); + const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CORP.key]))).filter(Boolean).sort(); filterBar.innerHTML = `
- +
- +
- `; container.appendChild(filterBar); @@ -34,21 +36,20 @@ export function renderMobileList(container: HTMLElement) { table.innerHTML = ` - No. - 상태 - 구매법인 - 자산코드 - 명칭 - 보관위치 - 관리자 - 구매연월 - 금액 + No. + ${ASSET_SCHEMA.STATUS.ui} + ${ASSET_SCHEMA.CORP.ui} + ${ASSET_SCHEMA.ASSET_CODE.ui} + ${ASSET_SCHEMA.MODEL.ui} + ${ASSET_SCHEMA.STORE_LOC.ui} + 담당자(정/부) + ${ASSET_SCHEMA.PURCHASE_YM.ui} + ${ASSET_SCHEMA.PRICE.ui} `; - tableWrapper.appendChild(table); container.appendChild(tableWrapper); const tbody = table.querySelector('tbody')!; @@ -61,14 +62,17 @@ export function renderMobileList(container: HTMLElement) { const corp = corpSelect ? corpSelect.value : ''; const filtered = fullList.filter(asset => { - const matchKeyword = !keyword || String(asset.자산코드||'').toLowerCase().includes(keyword) || String(asset.모델명||'').toLowerCase().includes(keyword) || String(asset.현사용조직||'').toLowerCase().includes(keyword); - const matchCorp = !corp || asset.법인 === corp; + const matchKeyword = !keyword || + String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) || + String(asset[ASSET_SCHEMA.MODEL.key]||'').toLowerCase().includes(keyword) || + String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword); + const matchCorp = !corp || asset[ASSET_SCHEMA.CORP.key] === corp; return matchKeyword && matchCorp; }); tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `검색 결과가 없습니다.`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } @@ -76,31 +80,33 @@ export function renderMobileList(container: HTMLElement) { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; - const statusColors: Record = { - '대여중': '#3b82f6', - '보관중': '#1E5149', - '수리중': '#ef4444', - '기타': '#6b7280' - }; - const statusColor = statusColors[asset.현재상태 || '보관중'] || '#6b7280'; - const statusBadge = `${asset.현재상태 || '보관중'}`; + const statusColors: Record = { '대여중': 'primary', '보관중': 'success', '수리중': 'danger', '기타': 'muted' }; + const statusValue = asset[ASSET_SCHEMA.STATUS.key] || '보관중'; + const statusType = statusColors[statusValue] || 'muted'; + const statusBadge = `${statusValue}`; + + const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || ''; + const subManager = asset[ASSET_SCHEMA.MANAGER_SUB.key] || ''; + const managerHtml = [ + mainManager ? `${createBadge('정', 'primary')} ${mainManager}` : '', + subManager ? `${createBadge('부', 'muted')} ${subManager}` : '' + ].filter(v => v !== '').join(' / '); tr.innerHTML = ` - ${idx + 1} - ${statusBadge} - ${asset.법인} - ${asset.자산코드 || '-'} - ${formatInline(asset.명칭 || asset.모델명)} - ${asset.보관위치 || '-'} - ${formatInline(asset.관리자 || asset.담당자_정)} - ${asset.구매일 || ''} - ${asset.금액 || '0'} + ${idx + 1} + ${statusBadge} + ${asset[ASSET_SCHEMA.CORP.key]} + ${asset[ASSET_SCHEMA.ASSET_CODE.key] || '-'} + ${formatInline(asset[ASSET_SCHEMA.MODEL.key] || asset.명칭)} + ${asset[ASSET_SCHEMA.STORE_LOC.key] || '-'} + ${managerHtml || '-'} + ${asset[ASSET_SCHEMA.PURCHASE_YM.key] || ''} + ${Number(asset[ASSET_SCHEMA.PRICE.key]||0).toLocaleString()} `; - tr.addEventListener('click', (e) => { - if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); - }); + tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); }); + createIcons({ icons: { RefreshCcw } }); }; document.getElementById('filter-keyword')?.addEventListener('input', updateTable); @@ -109,8 +115,7 @@ export function renderMobileList(container: HTMLElement) { (document.getElementById('filter-keyword') as HTMLInputElement).value = ''; (document.getElementById('filter-corp') as HTMLSelectElement).value = ''; updateTable(); - }); - - updateTable(); - } + }); + updateTable(); +} diff --git a/src/views/List/PcListView.ts b/src/views/List/PcListView.ts index 4b97b64..59d2cb0 100644 --- a/src/views/List/PcListView.ts +++ b/src/views/List/PcListView.ts @@ -1,26 +1,32 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, sortAssets } from '../../core/utils'; +import { formatInline, createBadge, sortAssets } from '../../core/utils'; +import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { createIcons, Paperclip, RefreshCcw } from 'lucide'; +/** + * PC 자산 목록 뷰 + * 담당자(부) 추가 및 정렬 보정 + */ export function renderPcList(container: HTMLElement) { const fullList = sortAssets(state.masterData.pc); const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - const corps = Array.from(new Set(fullList.map(a => a.법인))).filter(Boolean).sort(); + + const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CORP.key]))).filter(Boolean).sort(); filterBar.innerHTML = `
- +
- +
`; container.appendChild(filterBar); @@ -28,11 +34,30 @@ export function renderPcList(container: HTMLElement) { const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-container'; const table = document.createElement('table'); - table.innerHTML = `No구매법인현 사용조직자산코드사용자위치CPURAMStorage구매연월금액품의서관리`; + table.innerHTML = ` + + + No + ${ASSET_SCHEMA.CORP.ui} + ${ASSET_SCHEMA.ORG.ui} + ${ASSET_SCHEMA.ASSET_CODE.ui} + ${ASSET_SCHEMA.USER.ui} + ${ASSET_SCHEMA.LOCATION.ui} + 담당자(정/부) + ${ASSET_SCHEMA.MAINBOARD.ui} + ${ASSET_SCHEMA.CPU.ui} + ${ASSET_SCHEMA.RAM.ui} + Storage + ${ASSET_SCHEMA.PURCHASE_YM.ui} + ${ASSET_SCHEMA.PRICE.ui} + ${ASSET_SCHEMA.DOC_NAME.ui} + + + + `; tableWrapper.appendChild(table); container.appendChild(tableWrapper); - const tbody = table.querySelector('tbody')!; const updateTable = () => { @@ -43,41 +68,54 @@ export function renderPcList(container: HTMLElement) { const corp = corpSelect ? corpSelect.value : ''; const filtered = fullList.filter(asset => { - const matchKeyword = !keyword || String(asset.자산코드||'').toLowerCase().includes(keyword) || String(asset.사용자||'').toLowerCase().includes(keyword) || String(asset.현사용조직||'').toLowerCase().includes(keyword); - const matchCorp = !corp || asset.법인 === corp; + const matchKeyword = !keyword || + String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) || + String(asset[ASSET_SCHEMA.USER.key]||'').toLowerCase().includes(keyword) || + String(asset[ASSET_SCHEMA.ORG.key]||'').toLowerCase().includes(keyword) || + String(asset[ASSET_SCHEMA.MANAGER_MAIN.key]||'').toLowerCase().includes(keyword); + const matchCorp = !corp || asset[ASSET_SCHEMA.CORP.key] === corp; return matchKeyword && matchCorp; }); tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `검색 결과가 없습니다.`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } filtered.forEach((asset, idx) => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; - const storage = [asset.SSD1, asset.SSD2, asset.HDD1].filter(v => v).join(' / '); + const storage = [asset[ASSET_SCHEMA.STORAGE1.key], asset[ASSET_SCHEMA.STORAGE2.key]].filter(v => v).join(' / '); + + const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || ''; + const subManager = asset[ASSET_SCHEMA.MANAGER_SUB.key] || ''; + const managerHtml = [ + mainManager ? `${createBadge('정', 'primary')} ${mainManager}` : '', + subManager ? `${createBadge('부', 'muted')} ${subManager}` : '' + ].filter(v => v !== '').join(' / '); + tr.innerHTML = ` - ${idx+1} - ${asset.법인} - ${asset.현사용조직||''} - ${asset.자산코드} - ${asset.사용자||''} - ${asset.위치||''} - ${asset.CPU||''} - ${asset.RAM||''} - ${formatInline(storage)} - ${asset.구매연월 || asset.구매일 || ''} - ${asset.금액||''} - ${asset.품의서명 ? '' : '-'} - + ${idx+1} + ${asset[ASSET_SCHEMA.CORP.key]} + ${asset[ASSET_SCHEMA.ORG.key]||'-'} + ${asset[ASSET_SCHEMA.ASSET_CODE.key]} + ${asset[ASSET_SCHEMA.USER.key]||''} + ${asset[ASSET_SCHEMA.LOCATION.key]||''} + ${managerHtml || '-'} + ${asset[ASSET_SCHEMA.MAINBOARD.key]||'-'} + ${asset[ASSET_SCHEMA.CPU.key]||''} + ${asset[ASSET_SCHEMA.RAM.key]||''} + ${formatInline(storage)} + ${asset[ASSET_SCHEMA.PURCHASE_YM.key] || ''} + ${Number(asset[ASSET_SCHEMA.PRICE.key]||0).toLocaleString()} + ${asset[ASSET_SCHEMA.DOC_NAME.key] ? '' : '-'} `; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); }); + tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); }); - createIcons({ icons: { Paperclip } }); + createIcons({ icons: { Paperclip, RefreshCcw } }); }; document.getElementById('filter-keyword')?.addEventListener('input', updateTable); diff --git a/src/views/List/ServerListView.ts b/src/views/List/ServerListView.ts index d374f48..b1fa44c 100644 --- a/src/views/List/ServerListView.ts +++ b/src/views/List/ServerListView.ts @@ -1,31 +1,37 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; import { formatInline, createBadge, sortAssets } from '../../core/utils'; -import { createIcons, RefreshCcw, Edit2 } from 'lucide'; +import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; +import { createIcons, RefreshCcw } from 'lucide'; +/** + * 서버 자산 목록 뷰 + * 라인 정렬 보정 및 헤더 통일 + */ export function renderServerList(container: HTMLElement) { const fullList = sortAssets(state.masterData.server); const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - const corps = Array.from(new Set(fullList.map(a => a.법인))).filter(Boolean).sort(); - const orgUnits = Array.from(new Set(fullList.map(a => a.현사용조직))).filter(Boolean).sort(); + + const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CORP.key]))).filter(Boolean).sort(); + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.ORG.key]))).filter(Boolean).sort(); filterBar.innerHTML = `
- +
- +
- +
`; container.appendChild(filterBar); @@ -33,7 +39,21 @@ export function renderServerList(container: HTMLElement) { const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-container'; const table = document.createElement('table'); - table.innerHTML = `No구매법인현 사용조직자산번호용도상세설치위치담당자관리`; + table.innerHTML = ` + + + No + ${ASSET_SCHEMA.CORP.ui} + ${ASSET_SCHEMA.ORG.ui} + ${ASSET_SCHEMA.ASSET_CODE.ui} + 용도 + 상세 + ${ASSET_SCHEMA.LOCATION.ui} + 담당자(정/부) + + + + `; tableWrapper.appendChild(table); container.appendChild(tableWrapper); @@ -49,15 +69,18 @@ export function renderServerList(container: HTMLElement) { const orgUnit = orgSelect ? orgSelect.value : ''; const filtered = fullList.filter(asset => { - const matchKeyword = !keyword || String(asset.자산코드||'').toLowerCase().includes(keyword) || String(asset.현사용조직||'').toLowerCase().includes(keyword) || String(asset.모델명||'').toLowerCase().includes(keyword); - const matchCorp = !corp || asset.법인 === corp; - const matchOrg = !orgUnit || asset.현사용조직 === orgUnit; + const matchKeyword = !keyword || + String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) || + String(asset[ASSET_SCHEMA.ORG.key]||'').toLowerCase().includes(keyword) || + String(asset[ASSET_SCHEMA.MODEL.key]||'').toLowerCase().includes(keyword); + const matchCorp = !corp || asset[ASSET_SCHEMA.CORP.key] === corp; + const matchOrg = !orgUnit || asset[ASSET_SCHEMA.ORG.key] === orgUnit; return matchKeyword && matchCorp && matchOrg; }); tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `검색 결과가 없습니다.`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } @@ -65,27 +88,24 @@ export function renderServerList(container: HTMLElement) { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; - const mainManager = asset.담당자_정 || ''; - const subManager = asset.담당자_부 || ''; + const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || ''; + const subManager = asset[ASSET_SCHEMA.MANAGER_SUB.key] || ''; const managerHtml = [ mainManager ? `${createBadge('정', 'primary')} ${mainManager}` : '', subManager ? `${createBadge('부', 'muted')} ${subManager}` : '' ].filter(v => v !== '').join(' / '); tr.innerHTML = ` - ${idx+1} - ${asset.법인} - ${asset.현사용조직||'-'} - ${asset.자산코드} + ${idx+1} + ${asset[ASSET_SCHEMA.CORP.key]} + ${asset[ASSET_SCHEMA.ORG.key]||'-'} + ${asset[ASSET_SCHEMA.ASSET_CODE.key]} ${formatInline(asset.용도)} ${formatInline(asset.상세)} - ${formatInline(asset.위치)} - ${managerHtml} - - - + ${formatInline(asset[ASSET_SCHEMA.LOCATION.key])} + ${managerHtml || '-'} `; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); }); + tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); }); }; @@ -101,5 +121,5 @@ export function renderServerList(container: HTMLElement) { }); updateTable(); - createIcons({ icons: { RefreshCcw, Edit2 } }); + createIcons({ icons: { RefreshCcw } }); } diff --git a/src/views/List/StorageListView.ts b/src/views/List/StorageListView.ts index 0c922b0..405fe09 100644 --- a/src/views/List/StorageListView.ts +++ b/src/views/List/StorageListView.ts @@ -1,31 +1,37 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; import { formatInline, createBadge, sortAssets } from '../../core/utils'; +import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { createIcons, RefreshCcw } from 'lucide'; +/** + * 스토리지 자산 목록 뷰 + * 라인 정렬 보정 및 헤더 통일 + */ export function renderStorageList(container: HTMLElement) { const fullList = sortAssets(state.masterData.storage); const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; - const corps = Array.from(new Set(fullList.map(a => a.법인))).filter(Boolean).sort(); - const orgUnits = Array.from(new Set(fullList.map(a => a.현사용조직))).filter(Boolean).sort(); + + const corps = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.CORP.key]))).filter(Boolean).sort(); + const orgUnits = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.ORG.key]))).filter(Boolean).sort(); filterBar.innerHTML = `
- +
- +
- +
`; container.appendChild(filterBar); @@ -33,7 +39,21 @@ export function renderStorageList(container: HTMLElement) { const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-container'; const table = document.createElement('table'); - table.innerHTML = `No구매법인현 사용조직자산번호용도상세설치위치담당자모델명Storage관리`; + table.innerHTML = ` + + + No + ${ASSET_SCHEMA.CORP.ui} + ${ASSET_SCHEMA.ORG.ui} + ${ASSET_SCHEMA.ASSET_CODE.ui} + 용도 + 상세 + ${ASSET_SCHEMA.LOCATION.ui} + 담당자(정/부) + + + + `; tableWrapper.appendChild(table); container.appendChild(tableWrapper); @@ -49,15 +69,17 @@ export function renderStorageList(container: HTMLElement) { const orgUnit = orgSelect ? orgSelect.value : ''; const filtered = fullList.filter(asset => { - const matchKeyword = !keyword || String(asset.자산코드||'').toLowerCase().includes(keyword) || String(asset.현사용조직||'').toLowerCase().includes(keyword) || String(asset.모델명||'').toLowerCase().includes(keyword); - const matchCorp = !corp || asset.법인 === corp; - const matchOrg = !orgUnit || asset.현사용조직 === orgUnit; + const matchKeyword = !keyword || + String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) || + String(asset[ASSET_SCHEMA.ORG.key]||'').toLowerCase().includes(keyword); + const matchCorp = !corp || asset[ASSET_SCHEMA.CORP.key] === corp; + const matchOrg = !orgUnit || asset[ASSET_SCHEMA.ORG.key] === orgUnit; return matchKeyword && matchCorp && matchOrg; }); tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `검색 결과가 없습니다.`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } @@ -65,26 +87,24 @@ export function renderStorageList(container: HTMLElement) { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; - const mainManager = asset.담당자_정 || asset.관리자 || ''; - const subManager = asset.담당자_부 || ''; - const managerHtml = [mainManager ? `${createBadge('정', '#1E5149')} ${mainManager}` : '', subManager ? `${createBadge('부', '#9CA3AF')} ${subManager}` : ''].filter(v => v !== '').join(' / '); + const mainManager = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || ''; + const subManager = asset[ASSET_SCHEMA.MANAGER_SUB.key] || ''; + const managerHtml = [ + mainManager ? `${createBadge('정', 'primary')} ${mainManager}` : '', + subManager ? `${createBadge('부', 'muted')} ${subManager}` : '' + ].filter(v => v !== '').join(' / '); - const storage = [asset.SSD1, asset.SSD2, asset.용량].filter(v => v).join(' / '); - tr.innerHTML = ` - ${idx+1} - ${asset.법인} - ${asset.현사용조직||''} - ${asset.자산코드} + ${idx+1} + ${asset[ASSET_SCHEMA.CORP.key]} + ${asset[ASSET_SCHEMA.ORG.key]||'-'} + ${asset[ASSET_SCHEMA.ASSET_CODE.key]} ${formatInline(asset.용도)} ${formatInline(asset.상세)} - ${formatInline(asset.위치)} - ${managerHtml} - ${asset.모델명||''} - ${formatInline(storage)} - + ${formatInline(asset[ASSET_SCHEMA.LOCATION.key])} + ${managerHtml || '-'} `; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openHwModal(asset, 'view'); }); + tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); }); }; @@ -100,4 +120,5 @@ export function renderStorageList(container: HTMLElement) { }); updateTable(); + createIcons({ icons: { RefreshCcw } }); } diff --git a/src/views/List/SwListView.ts b/src/views/List/SwListView.ts index 41c1a81..abd3a9a 100644 --- a/src/views/List/SwListView.ts +++ b/src/views/List/SwListView.ts @@ -1,11 +1,14 @@ import { state } from '../../core/state'; import { openSwModal } from '../../components/Modal/SWModal'; -import { openSwUserModal } from '../../components/Modal/SWUserModal'; import { sortAssets } from '../../core/utils'; import { CORP_LIST } from '../../components/Modal/SharedData'; import { generateOptionsHTML } from '../../components/Modal/ModalUtils'; -import { createIcons, Edit2, Users, RefreshCcw } from 'lucide'; +import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; +import { createIcons, RefreshCcw } from 'lucide'; +/** + * 소프트웨어(구독/영구) 자산 목록 뷰 + */ export function renderSwList(container: HTMLElement) { const isSub = state.activeSubTab === '구독SW'; const fullList = sortAssets(isSub ? state.masterData.subSw : state.masterData.permSw); @@ -14,7 +17,7 @@ export function renderSwList(container: HTMLElement) { filterBar.className = 'search-bar'; filterBar.innerHTML = `
- +
@@ -28,11 +31,11 @@ export function renderSwList(container: HTMLElement) {
- +
`; container.appendChild(filterBar); @@ -46,15 +49,14 @@ export function renderSwList(container: HTMLElement) { No. 상태 분야 - 구매법인 + ${ASSET_SCHEMA.CORP.ui} 부서 - 제품명 - 구매연월 - ${isSub ? '구독일' : ''} - 금액 - 수량 + ${ASSET_SCHEMA.PRODUCT.ui} + ${ASSET_SCHEMA.PURCHASE_YM.ui} + ${isSub ? `${ASSET_SCHEMA.EXPIRY.ui}` : ''} + ${ASSET_SCHEMA.PRICE.ui} + ${ASSET_SCHEMA.QTY.ui} 사용가능 - 관리 @@ -74,28 +76,28 @@ export function renderSwList(container: HTMLElement) { const corp = corpSelect ? corpSelect.value : ''; const filtered = fullList.filter(asset => { - const matchKeyword = !keyword || (asset.제품명 || '').toLowerCase().includes(keyword) || (asset.부서 || '').toLowerCase().includes(keyword); + const matchKeyword = !keyword || (asset[ASSET_SCHEMA.PRODUCT.key] || '').toLowerCase().includes(keyword) || (asset.부서 || '').toLowerCase().includes(keyword); const matchField = !field || asset.분야 === field; - const matchCorp = !corp || asset.법인 === corp; + const matchCorp = !corp || asset[ASSET_SCHEMA.CORP.key] === corp; return matchKeyword && matchField && matchCorp; }); tbody.innerHTML = ''; if (filtered.length === 0) { - tbody.innerHTML = `검색 결과가 없습니다.`; + tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; return; } filtered.forEach((asset, idx) => { const assigned = state.masterData.swUsers.filter(u => u.sw_id === asset.id).length; - const qty = typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10); + const qty = typeof asset[ASSET_SCHEMA.QTY.key] === 'number' ? asset[ASSET_SCHEMA.QTY.key] : parseInt(asset[ASSET_SCHEMA.QTY.key]||'0', 10); const avail = qty - assigned; - let statusHtml = ''; + let statusBadge = ''; if (isSub) { let isExpired = false; - if (asset.구독일) { - const parts = asset.구독일.split('~'); + if (asset[ASSET_SCHEMA.EXPIRY.key]) { + const parts = asset[ASSET_SCHEMA.EXPIRY.key].split('~'); const endDateStr = parts[parts.length - 1].trim().replace(/\./g, '-'); const endDate = new Date(endDateStr); if (!isNaN(endDate.getTime())) { @@ -103,11 +105,9 @@ export function renderSwList(container: HTMLElement) { if (endDate < new Date()) isExpired = true; } } - if (isExpired) statusHtml = `만료`; - else statusHtml = `사용중`; + statusBadge = isExpired ? `만료` : `사용중`; } else { - if (asset.유지보수여부) statusHtml = `유효`; - else statusHtml = `없음`; + statusBadge = asset.유지보수여부 ? `유효` : `없음`; } const tr = document.createElement('tr'); @@ -115,35 +115,22 @@ export function renderSwList(container: HTMLElement) { tr.innerHTML = ` ${idx+1} - ${statusHtml} - ${asset.분야||''} - ${asset.법인} - ${asset.부서||''} - ${asset.제품명} - ${asset.구매일||''} - ${isSub ? `${asset.구독일||''}` : ''} - ${asset.금액||'0'} + ${statusBadge} + ${asset.분야||''} + ${asset[ASSET_SCHEMA.CORP.key]} + ${asset.부서||''} + ${asset[ASSET_SCHEMA.PRODUCT.key]} + ${asset[ASSET_SCHEMA.PURCHASE_YM.key]||''} + ${isSub ? `${asset[ASSET_SCHEMA.EXPIRY.key]||''}` : ''} + ${Number(asset[ASSET_SCHEMA.PRICE.key]||0).toLocaleString()} ${qty} ${avail} - - - - `; - tr.addEventListener('click', (e) => { - if (!(e.target as HTMLElement).closest('button')) { - openSwModal(asset, 'view'); - } - }); - tr.querySelector('.btn-edit')?.addEventListener('click', (e) => { - e.stopPropagation(); - openSwModal(asset, 'edit'); - }); - tr.querySelector('.btn-users')?.addEventListener('click', (e) => { e.stopPropagation(); openSwUserModal(asset); }); + tr.addEventListener('click', () => openSwModal(asset, 'view')); tbody.appendChild(tr); }); - createIcons({ icons: { Edit2, Users, RefreshCcw } }); + createIcons({ icons: { RefreshCcw } }); }; document.getElementById('filter-keyword')?.addEventListener('input', updateTable); diff --git a/src/views/SW_Table.ts b/src/views/SW_Table.ts index adf666b..4802425 100644 --- a/src/views/SW_Table.ts +++ b/src/views/SW_Table.ts @@ -34,11 +34,16 @@ export function renderSWTable(mainContent: HTMLElement) { } else if (state.activeCategory === 'sw') { if (tab === '구독SW' || tab === '영구SW') { renderSwList(container); - } else if (tab === '클라우드') { - renderCloudList(container); } else { container.innerHTML = `
"${tab}" 탭에 대한 소프트웨어 리스트 뷰가 정의되지 않았습니다.
`; } + } else if (state.activeCategory === 'ops') { + // 운영 서비스 관련 탭 처리 + if (['도메인', '메일', '메신저', '청구비용'].includes(tab)) { + renderCloudList(container); // 일단 클라우드 리스트로 공통 처리 + } else { + container.innerHTML = `
"${tab}" 탭에 대한 운영 서비스 뷰가 정의되지 않았습니다.
`; + } } mainContent.appendChild(container);