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 = `