From 818beae0dfb2dba1a84bbab8b95837f182d9434f Mon Sep 17 00:00:00 2001 From: JooWangi Date: Tue, 14 Apr 2026 14:09:28 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20S/W=20=EC=9E=90=EC=82=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=9A=A9=20=EA=B2=80=EC=83=89/=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EB=B0=94=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - S/W 테이블 상단에 제품명/부서 및 분야/법인 필터링 바 추가 - 조회 바와 테이블을 각각 별도의 카드 스타일로 분리하여 시각적 완성도 높임 - 하드웨어 테이블에도 일관된 카드 레이아웃 구조 적용 --- src/style.css | 61 ++++++++++++++++ src/views/AssetTableView.ts | 140 +++++++++++++++++++++++++++++++----- 2 files changed, 182 insertions(+), 19 deletions(-) diff --git a/src/style.css b/src/style.css index 0bcbce1..7f0e5c4 100644 --- a/src/style.css +++ b/src/style.css @@ -356,3 +356,64 @@ tbody tr:hover { background-color: var(--bg-color); } display: flex; justify-content: space-between; align-items: center; } .footer-actions { display: flex; gap: 0.5rem; } + +/* Search Filter Bar */ +.search-bar { + display: flex; + flex-wrap: wrap; + gap: 1rem; + background-color: var(--white); + padding: 1.25rem; + border: 1px solid var(--border-color); + border-radius: 8px; + margin-bottom: 2rem; + align-items: flex-end; +} + +.search-item { + display: flex; + flex-direction: column; + gap: 0.5rem; + min-width: 180px; +} + +.search-item.flex-1 { + flex: 1; + min-width: 250px; +} + +.search-item label { + font-size: 0.75rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.search-item input, +.search-item select { + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 0.875rem; + outline: none; + transition: all 0.2s; + background-color: var(--white); +} + +.search-item input:focus, +.search-item select:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(30, 81, 73, 0.1); +} + +.btn-reset { + height: 36px; + padding: 0 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + color: var(--text-muted); +} + diff --git a/src/views/AssetTableView.ts b/src/views/AssetTableView.ts index 99184c0..1a0d4d4 100644 --- a/src/views/AssetTableView.ts +++ b/src/views/AssetTableView.ts @@ -11,7 +11,7 @@ import { openSwUserModal } from '../components/Modal/SWUserModal'; */ export function renderTable(mainContent: HTMLElement) { const container = document.createElement('div'); - container.className = 'table-container'; + container.className = 'view-container'; // 배경과 테두리가 없는 투명한 컨테이너 const table = document.createElement('table'); if (state.activeCategory === 'hw') { @@ -30,8 +30,11 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont const list = state.masterData.hw.filter(a => a.type === state.activeSubTab); if (state.activeSubTab === '개인PC') { + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-container'; table.innerHTML = `No법인자산코드사용자위치CPUGPURAMSSD1SSD2HDD1HDD2구매일금액납품업체품의서관리`; - container.appendChild(table); + tableWrapper.appendChild(table); + container.appendChild(tableWrapper); mainContent.appendChild(container); const tbody = document.getElementById('dynamic-tbody')!; if (list.length === 0) { tbody.innerHTML = `등록된 자산이 없습니다.`; return; } @@ -44,8 +47,11 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont tbody.appendChild(tr); }); } else if (state.activeSubTab === '스토리지') { + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-container'; table.innerHTML = `No법인유형자산코드명칭위치모델명용량담당자(정)IP주소구매일금액관리`; - container.appendChild(table); + tableWrapper.appendChild(table); + container.appendChild(tableWrapper); mainContent.appendChild(container); const tbody = document.getElementById('dynamic-tbody')!; if (list.length === 0) { tbody.innerHTML = `등록된 자산이 없습니다.`; return; } @@ -58,8 +64,11 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont tbody.appendChild(tr); }); } else { + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-container'; table.innerHTML = `No법인${state.activeSubTab === '전산비품' ? '유형' : ''}자산코드명칭위치관리자구매일금액관리`; - container.appendChild(table); + tableWrapper.appendChild(table); + container.appendChild(tableWrapper); mainContent.appendChild(container); const tbody = document.getElementById('dynamic-tbody')!; if (list.length === 0) { tbody.innerHTML = `등록된 자산이 없습니다.`; return; } @@ -75,25 +84,118 @@ function renderHwTable(table: HTMLTableElement, container: HTMLElement, mainCont } function renderSwTable(table: HTMLTableElement, container: HTMLElement, mainContent: HTMLElement) { - const list = state.masterData.sw.filter(a => a.type === state.activeSubTab); + const fullList = state.masterData.sw.filter(a => a.type === state.activeSubTab); const isSub = state.activeSubTab === '구독SW'; + + // 0. Container 준비 (조회 바 + 테이블) + container.innerHTML = ''; + // 1. 조회 바 (Filter Bar) 생성 + const filterBar = document.createElement('div'); + filterBar.className = 'search-bar'; + filterBar.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ + `; + container.appendChild(filterBar); + + // 2. 테이블 기본 구조 생성 + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'table-container'; table.classList.add('sw-table'); table.innerHTML = `No.분야법인부서제품명구매일${isSub ? '구독일' : ''}금액수량사용가능관리`; - container.appendChild(table); - mainContent.appendChild(container); - const tbody = document.getElementById('dynamic-tbody')!; - if (list.length === 0) { tbody.innerHTML = `정보가 없습니다.`; return; } - list.forEach((asset, idx) => { - const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length; - const avail = (typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10)) - assigned; - const tr = document.createElement('tr'); - tr.style.cursor = 'pointer'; - tr.innerHTML = `${idx+1}${asset.분야||''}${asset.법인}${asset.부서||''}${asset.제품명}${asset.구매일||''}${isSub ? `${asset.구독일||''}` : ''}${asset.금액||'0'}${asset.수량}${avail}`; - tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); }); - tr.querySelector('.btn-edit')?.addEventListener('click', () => openSwModal(asset)); - tr.querySelector('.btn-users')?.addEventListener('click', () => openSwUserModal(asset)); - tbody.appendChild(tr); + tableWrapper.appendChild(table); + container.appendChild(tableWrapper); + mainContent.appendChild(container); + + const tbody = document.getElementById('dynamic-tbody')!; + + // 3. 필터링 및 테이블 업데이트 로직 + const updateTable = () => { + const keyword = (document.getElementById('filter-keyword') as HTMLInputElement).value.toLowerCase().trim(); + const field = (document.getElementById('filter-field') as HTMLSelectElement).value; + const corp = (document.getElementById('filter-corp') as HTMLSelectElement).value; + + const 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; + }); + + tbody.innerHTML = ''; + if (filtered.length === 0) { + tbody.innerHTML = `검색 결과가 없습니다.`; + return; + } + + filtered.forEach((asset, idx) => { + const assigned = state.masterData.swUsers.filter(u => u.swId === asset.id).length; + const avail = (typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10)) - assigned; + const tr = document.createElement('tr'); + tr.style.cursor = 'pointer'; + tr.innerHTML = `${idx+1}${asset.분야||''}${asset.법인}${asset.부서||''}${asset.제품명}${asset.구매일||''}${isSub ? `${asset.구독일||''}` : ''}${asset.금액||'0'}${asset.수량}${avail}`; + tr.addEventListener('click', (e) => { if (!(e.target as HTMLElement).closest('button')) openSwModal(asset); }); + tr.querySelector('.btn-edit')?.addEventListener('click', () => openSwModal(asset)); + tr.querySelector('.btn-users')?.addEventListener('click', () => openSwUserModal(asset)); + tbody.appendChild(tr); + }); + + // 버튼 내 아이콘 다시 그리기 + createIcons({ + icons: { Edit2, Users, RefreshCcw: CalendarClock } // RefreshCcw는 아래 버튼용 + }); + // 초기화 버튼 아이콘은 별도로 + createIcons({ + scope: filterBar + }); + }; + + // 4. 이벤트 바인딩 + const keywordInput = document.getElementById('filter-keyword') as HTMLInputElement; + const fieldSelect = document.getElementById('filter-field') as HTMLSelectElement; + const corpSelect = document.getElementById('filter-corp') as HTMLSelectElement; + const resetBtn = document.getElementById('btn-reset-filters') as HTMLButtonElement; + + keywordInput.addEventListener('input', updateTable); + fieldSelect.addEventListener('change', updateTable); + corpSelect.addEventListener('change', updateTable); + + resetBtn.addEventListener('click', () => { + keywordInput.value = ''; + fieldSelect.value = ''; + corpSelect.value = ''; + updateTable(); }); + + // 초기 실행 + updateTable(); } +