diff --git a/index.html b/index.html index 378e833..9b13fd3 100644 --- a/index.html +++ b/index.html @@ -61,6 +61,7 @@ diff --git a/src/components/Navigation.ts b/src/components/Navigation.ts index 5565586..2533208 100644 --- a/src/components/Navigation.ts +++ b/src/components/Navigation.ts @@ -3,11 +3,11 @@ import { state } from '../core/state'; const MENU_CONFIG = { hw: { label: '하드웨어', - tabs: ['대시보드', '개인PC', '서버', '스토리지', '전산비품', '모바일기기'] + tabs: ['대시보드', '서버', '개인PC', '모바일기기', '스토리지', '전산비품'] }, sw: { label: '소프트웨어', - tabs: ['대시보드', '구독SW', '영구SW', '클라우드'] + tabs: ['대시보드', '구독SW', '영구SW'] }, ops: { label: '운영 서비스', diff --git a/src/core/tableHandler.ts b/src/core/tableHandler.ts new file mode 100644 index 0000000..6b00041 --- /dev/null +++ b/src/core/tableHandler.ts @@ -0,0 +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.onclick = () => { + let nextDirection: SortDirection = 'asc'; + + if (currentState.key === key) { + nextDirection = currentState.direction === 'asc' ? 'desc' : 'asc'; + } + + onSort(key, nextDirection); + }; + }); +} diff --git a/src/core/utils.ts b/src/core/utils.ts index f30381d..4e593c9 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -71,22 +71,55 @@ export function getAssetChanges(oldAsset: any, newAsset: any, fields: {key: stri } /** - * 자산 목록 정렬 (방안 C: 구매법인별 -> 자산번호 순) + * 자산 목록 정렬 (기본: 법인별 -> 자산번호 순) */ export function sortAssets(list: T[]): T[] { return [...list].sort((a: any, b: any) => { - // 1순위: 구매법인 (한글 가나다순) - const corpA = String(a.법인 || '').trim(); - const corpB = String(b.법인 || '').trim(); + // 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.자산번호 || '').trim(); - const codeB = String(b.자산코드 || b.자산번호 || '').trim(); + // 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(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; + }); +} diff --git a/src/main.ts b/src/main.ts index d999c7a..845e314 100644 --- a/src/main.ts +++ b/src/main.ts @@ -163,6 +163,14 @@ function initApp() { } }); + // 시크릿 클라우드 트리거 + document.getElementById('secret-cloud-trigger')?.addEventListener('click', () => { + state.activeCategory = 'sw'; + state.activeSubTab = '클라우드'; + const mainContent = document.getElementById('main-content')!; + renderSWTable(mainContent); + }); + createIcons({ 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/table.css b/src/styles/table.css index 357d9ec..f6f5f2a 100644 --- a/src/styles/table.css +++ b/src/styles/table.css @@ -64,11 +64,14 @@ background-color: var(--white); border-top: 1px solid var(--border-color); overflow: auto; + position: relative; + -webkit-overflow-scrolling: touch; } table { width: 100%; - border-collapse: collapse; + border-collapse: separate; + border-spacing: 0; table-layout: auto; } @@ -79,15 +82,21 @@ th, td { white-space: nowrap; } +thead { + position: sticky; + top: 0; + z-index: 50; +} + th { - background-color: #FAFAFA; + background-color: #FAFAFA !important; font-size: 13px; font-weight: 600; color: var(--text-muted); position: sticky; top: 0; - z-index: 10; - box-shadow: inset 0 -1px 0 var(--border-color); + z-index: 50; + box-shadow: inset 0 1px 0 var(--border-color), inset 0 -1px 0 var(--border-color); /* 상하 테두리 보정 */ text-transform: none; } @@ -123,3 +132,40 @@ tbody tr:hover { width: 16px; height: 16px; } + +/* --- Table Sorting --- */ +th.sortable { + cursor: pointer; + user-select: none; + transition: background-color 0.2s; + position: relative; + padding-right: 1.8rem !important; /* 아이콘 공간 확보 */ +} + +th.sortable:hover { + background-color: #F3F4F6; + color: var(--primary-color); +} + +th.sortable::after { + content: '↕'; + position: absolute; + right: 0.6rem; + top: 50%; + transform: translateY(-50%); + font-size: 11px; + opacity: 0.3; + transition: all 0.2s; +} + +th.sortable.asc::after { + content: '▲'; + opacity: 1; + color: var(--primary-color); +} + +th.sortable.desc::after { + content: '▼'; + opacity: 1; + color: var(--primary-color); +} diff --git a/src/views/Dashboard/HwDashboard.ts b/src/views/Dashboard/HwDashboard.ts index c1c5e5b..9c43aed 100644 --- a/src/views/Dashboard/HwDashboard.ts +++ b/src/views/Dashboard/HwDashboard.ts @@ -65,22 +65,25 @@ export function renderHwDashboard(container: HTMLElement) { container.innerHTML = `
-
-
전체 평균 사용 연수
-
${avgAge}
- +
+ 전체 평균 사용 연수 +
전체 자산 기준 (권장 4.5년)
+
${avgAge}년
+
-
-
5년 이상 노후 자산 비율
-
${over5Rate}%
- +
+ 5년 이상 노후 자산 비율 +
총 ${over5YearsCount}대 해당
+
${over5Rate}%
+
-
-
최신 도입 모델 (${latestYear}년)
-
+
+ 최신 도입 모델 (${latestYear}년) +
자산번호: ${(latestAsset as any)?.자산코드 || '-'}
+
${(latestAsset as any)?.모델명 || '정보 없음'}
- +
diff --git a/src/views/Dashboard/SwDashboard.ts b/src/views/Dashboard/SwDashboard.ts index 661d765..4146dfa 100644 --- a/src/views/Dashboard/SwDashboard.ts +++ b/src/views/Dashboard/SwDashboard.ts @@ -11,7 +11,6 @@ export function renderSwDashboard(container: HTMLElement) { let subCost2026 = 0; let permCost2026 = 0; - let cloudCost2026 = 0; const currentYear = new Date().getFullYear(); @@ -22,8 +21,8 @@ export function renderSwDashboard(container: HTMLElement) { const costByCat: Record = {}; categories.forEach(c => costByCat[c] = 0); - // 통합 SW 데이터 - const allSw = [...state.masterData.subSw, ...state.masterData.permSw, ...state.masterData.cloud]; + // 통합 SW 데이터 (클라우드 제외) + const allSw = [...state.masterData.subSw, ...state.masterData.permSw]; allSw.forEach(sw => { const userMapping = state.masterData.swUsers.find(u => u.sw_id === sw.id); @@ -44,7 +43,6 @@ export function renderSwDashboard(container: HTMLElement) { if (sw.구매일 && sw.구매일.startsWith('2026')) { if (sw.type === '구독SW') subCost2026 += price; else if (sw.type === '영구SW') permCost2026 += price; - else if (sw.type === '클라우드') cloudCost2026 += price; if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price; if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price; @@ -60,7 +58,6 @@ export function renderSwDashboard(container: HTMLElement) { const cost = Number(log.cost) || 0; if (asset.type === '구독SW') subCost2026 += cost; else if (asset.type === '영구SW') permCost2026 += cost; - else if (asset.type === '클라우드') cloudCost2026 += cost; if (costByCorp[asset.법인] !== undefined) costByCorp[asset.법인] += cost; if (asset.분야 && costByCat[asset.분야] !== undefined) costByCat[asset.분야] += cost; @@ -124,7 +121,7 @@ export function renderSwDashboard(container: HTMLElement) {

2026년 누적 도입 비용 분석

-
+
구독 SW 누적 비용 (2026)
갱신 및 추가 비용 합계
@@ -137,12 +134,6 @@ export function renderSwDashboard(container: HTMLElement) {
₩ ${permCost2026.toLocaleString()}
-
- 클라우드 누적 비용 (2026) -
월별 청구액 누적 합계
-
₩ ${cloudCost2026.toLocaleString()}
-
-
diff --git a/src/views/List/CloudListView.ts b/src/views/List/CloudListView.ts index 322ccd6..bf1336a 100644 --- a/src/views/List/CloudListView.ts +++ b/src/views/List/CloudListView.ts @@ -1,6 +1,8 @@ import { state } from '../../core/state'; import { openSwModal } from '../../components/Modal/SWModal'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; +import { dynamicSort } from '../../core/utils'; +import { setupTableSorting, SortState } from '../../core/tableHandler'; import { createIcons, Cloud, CreditCard, DollarSign, RefreshCcw } from 'lucide'; /** @@ -9,6 +11,7 @@ import { createIcons, Cloud, CreditCard, DollarSign, RefreshCcw } from 'lucide'; */ export function renderCloudList(container: HTMLElement) { const getFullList = () => state.masterData.cloud || []; + let sortState: SortState = { key: '', direction: 'asc' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; @@ -37,15 +40,15 @@ export function renderCloudList(container: HTMLElement) { table.innerHTML = ` - 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} + 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} @@ -63,7 +66,7 @@ export function renderCloudList(container: HTMLElement) { const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; const payment = paymentSelect ? paymentSelect.value : ''; - const filtered = getFullList().filter(asset => { + let filtered = getFullList().filter(asset => { const kwMatch = !keyword || (asset[ASSET_SCHEMA.PRODUCT.key] || '').toLowerCase().includes(keyword) || (asset.부서 || '').toLowerCase().includes(keyword) || @@ -72,6 +75,10 @@ export function renderCloudList(container: HTMLElement) { return kwMatch && payMatch; }); + if (sortState.key) { + filtered = dynamicSort(filtered, sortState.key, sortState.direction); + } + tbody.innerHTML = ''; if (filtered.length === 0) { tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; @@ -105,6 +112,12 @@ export function renderCloudList(container: HTMLElement) { tr.addEventListener('click', () => openSwModal(asset, 'view')); tbody.appendChild(tr); }); + + setupTableSorting(table, sortState, (key, dir) => { + sortState = { key, direction: dir }; + updateTable(); + }); + createIcons({ icons: { Cloud, CreditCard, DollarSign, RefreshCcw } }); }; diff --git a/src/views/List/DomainListView.ts b/src/views/List/DomainListView.ts index 9b9a202..aa04dd5 100644 --- a/src/views/List/DomainListView.ts +++ b/src/views/List/DomainListView.ts @@ -1,11 +1,15 @@ import { state } from '../../core/state'; -import { formatPrice } from '../../core/utils'; +import { formatPrice, dynamicSort } from '../../core/utils'; import { createIcons, Plus, Edit2, Trash2 } from 'lucide'; import { openDomainModal } from '../../components/Modal/DomainModal'; +import { setupTableSorting, SortState } from '../../core/tableHandler'; export function renderDomainList(container: HTMLElement) { container.innerHTML = ''; + let sortState: SortState = { key: '', direction: 'asc' }; + const fullList = state.masterData.domain; + const header = document.createElement('div'); header.className = 'list-header'; header.innerHTML = ` @@ -17,58 +21,70 @@ export function renderDomainList(container: HTMLElement) { const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-container'; - const table = document.createElement('table'); table.innerHTML = ` No. - 유형 - 법인 - 서비스명 - 관리도메인 - 시작일 - 만료일 - 금액 - 담당자 - 담당자(부) + 유형 + 법인 + 서비스명 + 관리도메인 + 시작일 + 만료일 + 금액 + 담당자 + 담당자(부) 비고 - - ${state.masterData.domain.length === 0 ? ` - - 등록된 도메인 정보가 없습니다. - - ` : state.masterData.domain.map((item, idx) => ` - - ${idx + 1} - ${item.type} - ${item.corp || ''} - ${item.service_name || ''} - ${item.domain_name || ''} - ${item.start_date || ''} - ${item.expiry_date || ''} - ${formatPrice(item.price)} - ${item.manager_main || ''} - ${item.manager_sub || ''} - ${item.remarks || ''} - - `).join('')} - + `; tableWrapper.appendChild(table); container.appendChild(tableWrapper); + const tbody = table.querySelector('tbody')!; - // 이벤트 바인딩 - table.querySelectorAll('.domain-row').forEach(row => { - row.addEventListener('click', () => { - const id = row.getAttribute('data-id'); - const item = state.masterData.domain.find(d => d.id === id); - if (item) openDomainModal(item); + const updateTable = () => { + let filtered = [...fullList]; + + if (sortState.key) { + filtered = dynamicSort(filtered, sortState.key, sortState.direction); + } + + tbody.innerHTML = ''; + if (filtered.length === 0) { + tbody.innerHTML = `등록된 도메인 정보가 없습니다.`; + return; + } + + filtered.forEach((item, idx) => { + const tr = document.createElement('tr'); + tr.className = 'domain-row'; + tr.style.cursor = 'pointer'; + tr.innerHTML = ` + ${idx + 1} + ${item.type} + ${item.corp || ''} + ${item.service_name || ''} + ${item.domain_name || ''} + ${item.start_date || ''} + ${item.expiry_date || ''} + ${formatPrice(item.price)} + ${item.manager_main || ''} + ${item.manager_sub || ''} + ${item.remarks || ''} + `; + tr.addEventListener('click', () => openDomainModal(item)); + tbody.appendChild(tr); }); - }); + setupTableSorting(table, sortState, (key, dir) => { + sortState = { key, direction: dir }; + updateTable(); + }); + }; + + updateTable(); createIcons({ icons: { Plus, Edit2, Trash2 } }); } diff --git a/src/views/List/EquipmentListView.ts b/src/views/List/EquipmentListView.ts index f8c8f6e..797255e 100644 --- a/src/views/List/EquipmentListView.ts +++ b/src/views/List/EquipmentListView.ts @@ -1,7 +1,8 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, createBadge, sortAssets } from '../../core/utils'; +import { formatInline, createBadge, sortAssets, dynamicSort } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; +import { setupTableSorting, SortState } from '../../core/tableHandler'; import { createIcons, RefreshCcw } from 'lucide'; /** @@ -10,6 +11,7 @@ import { createIcons, RefreshCcw } from 'lucide'; */ export function renderEquipmentList(container: HTMLElement) { const fullList = sortAssets(state.masterData.equip); + let sortState: SortState = { key: '', direction: 'asc' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; @@ -36,16 +38,16 @@ export function renderEquipmentList(container: HTMLElement) { table.innerHTML = ` - 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} + 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} @@ -62,7 +64,7 @@ export function renderEquipmentList(container: HTMLElement) { const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; const corp = corpSelect ? corpSelect.value : ''; - const filtered = fullList.filter(asset => { + let filtered = fullList.filter(asset => { const matchKeyword = !keyword || String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) || String(asset[ASSET_SCHEMA.MODEL.key]||'').toLowerCase().includes(keyword) || @@ -71,6 +73,10 @@ export function renderEquipmentList(container: HTMLElement) { return matchKeyword && matchCorp; }); + if (sortState.key) { + filtered = dynamicSort(filtered, sortState.key, sortState.direction); + } + tbody.innerHTML = ''; if (filtered.length === 0) { tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; @@ -108,6 +114,12 @@ export function renderEquipmentList(container: HTMLElement) { tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); }); + + setupTableSorting(table, sortState, (key, dir) => { + sortState = { key, direction: dir }; + updateTable(); + }); + createIcons({ icons: { RefreshCcw } }); }; diff --git a/src/views/List/MobileListView.ts b/src/views/List/MobileListView.ts index 8a9c7d5..25d45f9 100644 --- a/src/views/List/MobileListView.ts +++ b/src/views/List/MobileListView.ts @@ -1,7 +1,8 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, createBadge, sortAssets } from '../../core/utils'; +import { formatInline, createBadge, sortAssets, dynamicSort } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; +import { setupTableSorting, SortState } from '../../core/tableHandler'; import { createIcons, RefreshCcw } from 'lucide'; /** @@ -10,6 +11,7 @@ import { createIcons, RefreshCcw } from 'lucide'; */ export function renderMobileList(container: HTMLElement) { const fullList = sortAssets(state.masterData.mobile); + let sortState: SortState = { key: '', direction: 'asc' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; @@ -36,15 +38,15 @@ export function renderMobileList(container: HTMLElement) { table.innerHTML = ` - 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} + 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,7 +63,7 @@ export function renderMobileList(container: HTMLElement) { const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; const corp = corpSelect ? corpSelect.value : ''; - const filtered = fullList.filter(asset => { + let filtered = fullList.filter(asset => { const matchKeyword = !keyword || String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) || String(asset[ASSET_SCHEMA.MODEL.key]||'').toLowerCase().includes(keyword) || @@ -70,6 +72,10 @@ export function renderMobileList(container: HTMLElement) { return matchKeyword && matchCorp; }); + if (sortState.key) { + filtered = dynamicSort(filtered, sortState.key, sortState.direction); + } + tbody.innerHTML = ''; if (filtered.length === 0) { tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; @@ -106,6 +112,12 @@ export function renderMobileList(container: HTMLElement) { tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); }); + + setupTableSorting(table, sortState, (key, dir) => { + sortState = { key, direction: dir }; + updateTable(); + }); + createIcons({ icons: { RefreshCcw } }); }; diff --git a/src/views/List/PcListView.ts b/src/views/List/PcListView.ts index 59d2cb0..d7fde27 100644 --- a/src/views/List/PcListView.ts +++ b/src/views/List/PcListView.ts @@ -1,7 +1,8 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, createBadge, sortAssets } from '../../core/utils'; +import { formatInline, createBadge, sortAssets, dynamicSort } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; +import { setupTableSorting, SortState } from '../../core/tableHandler'; import { createIcons, Paperclip, RefreshCcw } from 'lucide'; /** @@ -10,6 +11,7 @@ import { createIcons, Paperclip, RefreshCcw } from 'lucide'; */ export function renderPcList(container: HTMLElement) { const fullList = sortAssets(state.masterData.pc); + let sortState: SortState = { key: '', direction: 'asc' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; @@ -37,19 +39,19 @@ export function renderPcList(container: HTMLElement) { 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} + 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} @@ -67,7 +69,7 @@ export function renderPcList(container: HTMLElement) { const keyword = keywordInput ? keywordInput.value.toLowerCase().trim() : ''; const corp = corpSelect ? corpSelect.value : ''; - const filtered = fullList.filter(asset => { + let filtered = fullList.filter(asset => { const matchKeyword = !keyword || String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) || String(asset[ASSET_SCHEMA.USER.key]||'').toLowerCase().includes(keyword) || @@ -77,6 +79,10 @@ export function renderPcList(container: HTMLElement) { return matchKeyword && matchCorp; }); + if (sortState.key) { + filtered = dynamicSort(filtered, sortState.key, sortState.direction); + } + tbody.innerHTML = ''; if (filtered.length === 0) { tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; @@ -115,6 +121,12 @@ export function renderPcList(container: HTMLElement) { tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); }); + + setupTableSorting(table, sortState, (key, dir) => { + sortState = { key, direction: dir }; + updateTable(); + }); + createIcons({ icons: { Paperclip, RefreshCcw } }); }; diff --git a/src/views/List/ServerListView.ts b/src/views/List/ServerListView.ts index b1fa44c..158e6e5 100644 --- a/src/views/List/ServerListView.ts +++ b/src/views/List/ServerListView.ts @@ -1,7 +1,8 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, createBadge, sortAssets } from '../../core/utils'; +import { formatInline, createBadge, sortAssets, dynamicSort } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; +import { setupTableSorting, SortState } from '../../core/tableHandler'; import { createIcons, RefreshCcw } from 'lucide'; /** @@ -10,6 +11,7 @@ import { createIcons, RefreshCcw } from 'lucide'; */ export function renderServerList(container: HTMLElement) { const fullList = sortAssets(state.masterData.server); + let sortState: SortState = { key: '', direction: 'asc' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; @@ -42,14 +44,14 @@ export function renderServerList(container: HTMLElement) { table.innerHTML = ` - No - ${ASSET_SCHEMA.CORP.ui} - ${ASSET_SCHEMA.ORG.ui} - ${ASSET_SCHEMA.ASSET_CODE.ui} - 용도 - 상세 - ${ASSET_SCHEMA.LOCATION.ui} - 담당자(정/부) + No + ${ASSET_SCHEMA.CORP.ui} + ${ASSET_SCHEMA.ORG.ui} + ${ASSET_SCHEMA.ASSET_CODE.ui} + 용도 + 상세 + ${ASSET_SCHEMA.LOCATION.ui} + 담당자(정/부) @@ -68,7 +70,7 @@ export function renderServerList(container: HTMLElement) { const corp = corpSelect ? corpSelect.value : ''; const orgUnit = orgSelect ? orgSelect.value : ''; - const filtered = fullList.filter(asset => { + let filtered = fullList.filter(asset => { const matchKeyword = !keyword || String(asset[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) || String(asset[ASSET_SCHEMA.ORG.key]||'').toLowerCase().includes(keyword) || @@ -78,6 +80,10 @@ export function renderServerList(container: HTMLElement) { return matchKeyword && matchCorp && matchOrg; }); + if (sortState.key) { + filtered = dynamicSort(filtered, sortState.key, sortState.direction); + } + tbody.innerHTML = ''; if (filtered.length === 0) { tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; @@ -108,6 +114,11 @@ export function renderServerList(container: HTMLElement) { tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); }); + + setupTableSorting(table, sortState, (key, dir) => { + sortState = { key, direction: dir }; + updateTable(); + }); }; document.getElementById('filter-keyword')?.addEventListener('input', updateTable); diff --git a/src/views/List/StorageListView.ts b/src/views/List/StorageListView.ts index 335a836..1ba89d7 100644 --- a/src/views/List/StorageListView.ts +++ b/src/views/List/StorageListView.ts @@ -1,7 +1,8 @@ import { state } from '../../core/state'; import { openHwModal } from '../../components/Modal/HWModal'; -import { formatInline, createBadge, sortAssets } from '../../core/utils'; +import { formatInline, createBadge, sortAssets, dynamicSort } from '../../core/utils'; import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; +import { setupTableSorting, SortState } from '../../core/tableHandler'; import { createIcons, RefreshCcw } from 'lucide'; /** @@ -10,6 +11,7 @@ import { createIcons, RefreshCcw } from 'lucide'; */ export function renderStorageList(container: HTMLElement) { const fullList = sortAssets(state.masterData.storage); + let sortState: SortState = { key: '', direction: 'asc' }; const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; @@ -42,14 +44,14 @@ export function renderStorageList(container: HTMLElement) { table.innerHTML = ` - No - ${ASSET_SCHEMA.CORP.ui} - ${ASSET_SCHEMA.ORG.ui} - ${ASSET_SCHEMA.ASSET_CODE.ui} - 용도 - 상세 - ${ASSET_SCHEMA.LOCATION.ui} - 담당자(정/부) + No + ${ASSET_SCHEMA.CORP.ui} + ${ASSET_SCHEMA.ORG.ui} + ${ASSET_SCHEMA.ASSET_CODE.ui} + 용도 + 상세 + ${ASSET_SCHEMA.LOCATION.ui} + 담당자(정/부) @@ -68,7 +70,7 @@ export function renderStorageList(container: HTMLElement) { const corp = corpSelect ? corpSelect.value : ''; const orgUnit = orgSelect ? orgSelect.value : ''; - const filtered = fullList.filter(asset => { + let filtered = fullList.filter(asset => { const matchKeyword = !keyword || String((asset as any)[ASSET_SCHEMA.ASSET_CODE.key]||'').toLowerCase().includes(keyword) || String((asset as any)[ASSET_SCHEMA.ORG.key]||'').toLowerCase().includes(keyword); @@ -77,6 +79,10 @@ export function renderStorageList(container: HTMLElement) { return matchKeyword && matchCorp && matchOrg; }); + if (sortState.key) { + filtered = dynamicSort(filtered, sortState.key, sortState.direction); + } + tbody.innerHTML = ''; if (filtered.length === 0) { tbody.innerHTML = `${UI_TEXT.MESSAGES.NO_DATA}`; @@ -107,6 +113,11 @@ export function renderStorageList(container: HTMLElement) { tr.addEventListener('click', () => openHwModal(asset, 'view')); tbody.appendChild(tr); }); + + setupTableSorting(table, sortState, (key, dir) => { + sortState = { key, direction: dir }; + updateTable(); + }); }; document.getElementById('filter-keyword')?.addEventListener('input', updateTable); diff --git a/src/views/List/SwListView.ts b/src/views/List/SwListView.ts index 49d93ed..f25d2d1 100644 --- a/src/views/List/SwListView.ts +++ b/src/views/List/SwListView.ts @@ -1,7 +1,8 @@ import { state } from '../../core/state'; import { openSwModal } from '../../components/Modal/SWModal'; import { openSwUserModal } from '../../components/Modal/SWUserModal'; -import { sortAssets, formatPrice } from '../../core/utils'; +import { sortAssets, dynamicSort, formatPrice } from '../../core/utils'; +import { setupTableSorting, SortState } from '../../core/tableHandler'; import { CORP_LIST } from '../../components/Modal/SharedData'; import { generateOptionsHTML } from '../../components/Modal/ModalUtils'; import { createIcons, Edit2, Users, RefreshCcw } from 'lucide'; @@ -10,6 +11,8 @@ export function renderSwList(container: HTMLElement) { const isSub = state.activeSubTab === '구독SW'; const fullList = sortAssets(isSub ? state.masterData.subSw : state.masterData.permSw); + let sortState: SortState = { key: '', direction: 'asc' }; + const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; filterBar.innerHTML = ` @@ -43,17 +46,17 @@ export function renderSwList(container: HTMLElement) { table.innerHTML = ` - No. - 상태 - 분야 - 법인 - 부서 - 제품명 - 구매일 - 시작일 - 만료일 - 금액 - 수량 + No. + 상태 + 분야 + 법인 + 부서 + 제품명 + 구매일 + 시작일 + 만료일 + 금액 + 수량 사용가능 사용자 @@ -74,13 +77,17 @@ export function renderSwList(container: HTMLElement) { const field = fieldSelect ? fieldSelect.value : ''; const corp = corpSelect ? corpSelect.value : ''; - const filtered = fullList.filter(asset => { + let filtered = fullList.filter(asset => { const matchKeyword = !keyword || (asset.제품명 || '').toLowerCase().includes(keyword) || (asset.부서 || '').toLowerCase().includes(keyword); const matchField = !field || asset.분야 === field; const matchCorp = !corp || asset.법인 === corp; return matchKeyword && matchField && matchCorp; }); + if (sortState.key) { + filtered = dynamicSort(filtered, sortState.key, sortState.direction); + } + tbody.innerHTML = ''; if (filtered.length === 0) { tbody.innerHTML = `검색 결과가 없습니다.`; @@ -155,6 +162,12 @@ export function renderSwList(container: HTMLElement) { }); tbody.appendChild(tr); }); + + setupTableSorting(table, sortState, (key, dir) => { + sortState = { key, direction: dir }; + updateTable(); + }); + createIcons({ icons: { Edit2, Users, RefreshCcw } }); };