From 587e92a7da7523a30de8d062b7c7ee0414eb4726 Mon Sep 17 00:00:00 2001 From: Taehoon Date: Fri, 19 Jun 2026 14:55:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=84=9C=EB=B2=84=20=ED=83=AD=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20=EC=8B=9C=20=EB=B7=B0=20=EB=AA=A8=EB=93=9C?= =?UTF-8?q?=20=EC=9C=A0=EC=A7=80=20=EB=B0=8F=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C/=EB=A7=B5=20=EC=97=90=EB=94=94=ED=84=B0=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=ED=91=9C=EC=A4=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서버 탭 복귀 시 최근 선택한 뷰 모드(목록/위치) 상태 유지 및 currentViewMode 상태 일원화 - 개인PC 대시보드 및 맵 에디터의 인라인 CSS 스타일을 공통 CSS 및 변수 클래스로 분리 및 가독성 개선 - Vite 멀티페이지 빌드 설정(vite.config.ts) 추가 --- map_editor.html | 10 +- src/main.ts | 10 +- src/styles/map-editor.css | 66 ++++++-- src/views/Dashboard/HwDashboard.ts | 246 +++++++++++++++-------------- src/views/List/ListFactory.ts | 17 +- src/views/LocationView.ts | 4 +- src/views/MapEditor.ts | 4 +- vite.config.ts | 9 ++ 8 files changed, 205 insertions(+), 161 deletions(-) diff --git a/map_editor.html b/map_editor.html index ea0b683..fde82d5 100644 --- a/map_editor.html +++ b/map_editor.html @@ -5,8 +5,10 @@ ITAM Map Coordinate Editor v3.0 + + - +
@@ -22,7 +24,7 @@ diff --git a/src/main.ts b/src/main.ts index 563f2d8..8c48d01 100644 --- a/src/main.ts +++ b/src/main.ts @@ -30,19 +30,17 @@ function refreshView(tab?: string) { return; } - // 서버 탭이 아닐 경우 '자산현황(위치)' 뷰 진입 방지 및 강제 리스트 모드 전환 - if (activeTab !== '서버' && state.viewMode === 'location') { - state.viewMode = 'list'; - } - + // 서버 탭이 아닐 경우에는 state.viewMode가 location이더라도 강제로 목록(list) 뷰를 그리도록 함 + // (state.viewMode의 원래 상태는 보존하여, 서버 탭 복귀 시 최근 보던 모드를 유지함) const isServerTab = activeTab === '서버'; + const effectiveViewMode = isServerTab ? state.viewMode : 'list'; mainContent.innerHTML = `
`; const viewBody = document.getElementById('view-body')!; - if (state.viewMode === 'location') { + if (effectiveViewMode === 'location') { renderLocationView(viewBody); } else { renderSWTable(viewBody); // 리스트 형식 diff --git a/src/styles/map-editor.css b/src/styles/map-editor.css index 5e4fdd6..031f31a 100644 --- a/src/styles/map-editor.css +++ b/src/styles/map-editor.css @@ -12,8 +12,8 @@ .folder-item { padding: 10px 15px; background: var(--bg-light); - font-weight: bold; - font-size: 13px; + font-weight: 700; + font-size: var(--fs-base); border-bottom: 1px solid var(--border-color); color: var(--primary-color); } @@ -21,13 +21,13 @@ .file-item { padding: 8px 25px; cursor: pointer; - font-size: 12px; + font-size: var(--fs-sm); border-bottom: 1px solid var(--bg-color); transition: background 0.2s; } .file-item:hover { background: var(--bg-light); } -.file-item.active { background: var(--primary-color); color: var(--white); font-weight: bold; } +.file-item.active { background: var(--primary-color); color: var(--white); font-weight: 700; } /* Center: Editor Area */ .editor-container { @@ -70,10 +70,10 @@ box-shadow: -5px 0 15px rgba(0,0,0,0.05); } -.sidebar h2 { margin-top: 0; color: var(--primary-color); font-size: 1.2rem; } -.sidebar p { font-size: 0.85rem; color: var(--text-muted); line-height: 1.4; margin-bottom: 20px; } +.sidebar h2 { margin-top: 0; color: var(--primary-color); font-size: var(--fs-lg); font-weight: 600; } +.sidebar p { font-size: var(--fs-sm); color: var(--text-muted); line-height: 1.4; margin-bottom: 20px; } -.current-path { font-size: 11px; color: var(--text-muted); margin-bottom: 10px; word-break: break-all; font-family: monospace; } +.current-path { font-size: var(--fs-xs); color: var(--text-muted); margin-bottom: 10px; word-break: break-all; font-family: monospace; } .box-list { flex: 1; @@ -87,7 +87,7 @@ .box-item { font-family: monospace; - font-size: 11px; + font-size: var(--fs-xs); padding: 10px 6px; border-bottom: 1px solid var(--border-color); display: flex; @@ -102,7 +102,7 @@ } .box-index { - font-weight: bold; + font-weight: 700; color: var(--primary-color); } @@ -128,7 +128,7 @@ padding: 2px 4px; border: 1px solid var(--border-color); border-radius: 2px; - font-size: 10px; + font-size: var(--fs-xs); outline: none; } @@ -137,7 +137,7 @@ } .box-item:hover { background: var(--white); } -.btn-del { cursor: pointer; color: var(--danger); border: none; background: none; font-size: 16px; padding: 0 5px; } +.btn-del { cursor: pointer; color: var(--danger); border: none; background: none; font-size: var(--fs-md); padding: 0 5px; } .actions { display: flex; flex-direction: column; gap: 8px; } @@ -174,8 +174,8 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - font-size: 10px; - font-weight: bold; + font-size: var(--fs-xs); + font-weight: 700; color: var(--primary-color); pointer-events: none; white-space: nowrap; @@ -192,9 +192,45 @@ #save-status { margin-top: 8px; - font-size: 11px; + font-size: var(--fs-xs); color: var(--success); text-align: center; - font-weight: bold; + font-weight: 700; height: 14px; } + +/* Editor Body & Layout Overrides */ +.editor-body { + margin: 0; + display: flex; + height: 100vh; + overflow: hidden; +} + +.editor-version { + font-size: var(--fs-xs); + color: var(--text-muted); +} + +.actions .btn { + height: 38px; +} + +/* Box Item Dropdown Inputs */ +.box-inputs.margin-bottom { + margin-bottom: 8px; +} + +.box-inputs select { + width: 100%; + padding: 4px; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: var(--fs-xs); + background-color: var(--canvas); + color: var(--text-main); + outline: none; +} +.box-inputs select:focus { + border-color: var(--primary-color); +} diff --git a/src/views/Dashboard/HwDashboard.ts b/src/views/Dashboard/HwDashboard.ts index 4e767cd..f4b4eef 100644 --- a/src/views/Dashboard/HwDashboard.ts +++ b/src/views/Dashboard/HwDashboard.ts @@ -66,19 +66,15 @@ export function renderHwDashboard(container: HTMLElement) { // 2. 1페이지 매거진 리포트(제목바 제거, '| 제목' 미니멀리즘 스타일) HTML 빌드 container.innerHTML = ` -
+
-
-

- 개인 PC 자산 대시보드 -

+
- 조직 필터:
@@ -98,37 +94,35 @@ export function renderHwDashboard(container: HTMLElement) {
-
-
- 보유 자산 수량 +
+
+ 보유 자산 수량
-
0대
+
0대
-
-
- 사양 부족 +
+
+ 사양 부족
-
0대
+
0대
-
-
- 오버 스펙 +
+
+ 오버 스펙
-
0대
+
0대
-
-
- 윈도우 11 불가 +
+
+ 윈도우 11 불가
-
0대
+
0대
@@ -137,10 +131,10 @@ export function renderHwDashboard(container: HTMLElement) {
-
+
-
- 조직별 사용 비율 +
+ 조직별 사용 비율
@@ -149,7 +143,7 @@ export function renderHwDashboard(container: HTMLElement) {
-
+
한맥 @@ -183,16 +177,16 @@ export function renderHwDashboard(container: HTMLElement) {
-
-
- PC 노후도 +
+
+ PC 노후도
- - - - - +
구분 (연한)보유
+ + + + @@ -207,24 +201,24 @@ export function renderHwDashboard(container: HTMLElement) { -
+
-
- 등급별 자산 종합현황 +
+ 등급별 자산 종합현황
-
구분 (연한)보유
- - - - - - - - +
구분 (등급)보유량운영중재고부족분사양 적정성
+ + + + + + + + @@ -237,10 +231,33 @@ export function renderHwDashboard(container: HTMLElement) { `; @@ -259,7 +276,7 @@ export function renderHwDashboard(container: HTMLElement) { const button = b as HTMLButtonElement; button.classList.remove('active'); button.style.background = 'transparent'; - button.style.color = '#475569'; + button.style.color = 'var(--text-muted)'; }); btn.classList.add('active'); @@ -483,8 +500,7 @@ function updateDashboardData(pcs: any[], selectedDept: string) { const data = matrix[gradeKey]; const totalRate = filtered.length > 0 ? Math.round((data.total / filtered.length) * 100) : 0; - const cellStyle = `padding: 22px 8px; text-align: center; font-weight: 700; cursor: pointer; transition: background 0.2s; font-size: 1.05rem;`; - const hoverEvents = `onmouseover="this.style.background='#F1F5F9'" onmouseout="this.style.background='none'"`; + const cellStyle = `padding: 22px 8px; text-align: center; font-weight: 700; font-size: var(--fs-base);`; // 사양 적정성 분석 데이터 계산 (운영중인 자산만) const { win11, under, normal, over } = getSpecStatusCounts(data.activePcs); @@ -503,31 +519,31 @@ function updateDashboardData(pcs: any[], selectedDept: string) { barGraphHtml = `
-
- ${win11 > 0 ? `
` : ''} - ${under > 0 ? `
` : ''} - ${normal > 0 ? `
` : ''} - ${over > 0 ? `
` : ''} +
+ ${win11 > 0 ? `
` : ''} + ${under > 0 ? `
` : ''} + ${normal > 0 ? `
` : ''} + ${over > 0 ? `
` : ''}
-
+
-
+
`; } else { - barGraphHtml = `운영중 자산 없음`; + barGraphHtml = `운영중 자산 없음`; } return ` -
- - - - - - + + + + + + @@ -561,31 +577,28 @@ function updateDashboardData(pcs: any[], selectedDept: string) { totBarGraphHtml = `
-
- ${totUnder > 0 ? `
` : ''} - ${totNormal > 0 ? `
` : ''} - ${totOver > 0 ? `
` : ''} +
+ ${totUnder > 0 ? `
` : ''} + ${totNormal > 0 ? `
` : ''} + ${totOver > 0 ? `
` : ''}
-
+
-
+
`; } else { - totBarGraphHtml = `운영중 자산 없음`; + totBarGraphHtml = `운영중 자산 없음`; } - const cellStyleHeader = `padding: 12px 10px; text-align: center; font-weight: 800; cursor: pointer; transition: background 0.2s; background: #F8FAFC; font-size: 1.05rem;`; - const hoverEventsHeader = `onmouseover="this.style.background='#EEF2F6'" onmouseout="this.style.background='#F8FAFC'"`; - matrixTbody.innerHTML = ` ${renderMatrixRow('premium', '최상급 PC (85점 이상)', '#11302B', premiumShortage)} ${renderMatrixRow('high', '상급 PC (70점 ~ 85점)', '#1E8E7C', highShortage)} ${renderMatrixRow('normal', '중급 PC (40점 ~ 70점)', '#10B981', normalShortage)} - ${renderMatrixRow('entry', '보급 PC (20점 ~ 40점)', '#F59E0B', entryShortage)} - ${renderMatrixRow('replace', '교체 대상 PC (20점 미만)', '#EF4444', replaceShortage)} + ${renderMatrixRow('entry', '보급 PC (20점 ~ 40점)', 'var(--color-orange)', entryShortage)} + ${renderMatrixRow('replace', '교체 대상 PC (20점 미만)', 'var(--danger)', replaceShortage)} `; // 셀별 동적 클릭 리스너 바인딩 @@ -709,9 +722,9 @@ function updateDashboardData(pcs: any[], selectedDept: string) { const renderAgingRow = (label: string, list: any[], ageGroupKey: string) => { return ` -
- - + + + `; }; @@ -738,14 +751,9 @@ function updateDashboardData(pcs: any[], selectedDept: string) { }); // 8. 요약 지표 카드 클릭 리스너 설정 - const bindCardClick = (id: string, gradeTitle: string, filterFn: (p: any) => boolean, hoverBgColor: string) => { + const bindCardClick = (id: string, gradeTitle: string, filterFn: (p: any) => boolean) => { const card = document.getElementById(id)!; if (!card) return; - card.style.cursor = 'pointer'; - card.style.transition = 'background-color 0.15s ease'; - - card.onmouseover = () => { card.style.backgroundColor = hoverBgColor; }; - card.onmouseout = () => { card.style.backgroundColor = '#ffffff'; }; card.onclick = () => { const pcsInGrade = filtered.filter(filterFn); @@ -754,9 +762,9 @@ function updateDashboardData(pcs: any[], selectedDept: string) { }; // 사양 부족 / 오버 스펙 / 윈도우 11 불가 클릭 리스너 설정 - bindCardClick('card-under-spec', '사양 부족 대상', p => p._spec_status === '사양 부족', '#FEF2F2'); - bindCardClick('card-over-spec', '오버 스펙 대상', p => p._spec_status === '오버스펙', '#FFFBEB'); - bindCardClick('card-win11-incompatible', '윈도우 11 업그레이드 불가 PC', p => isWindows11Incompatible(p.cpu, p.ram), '#F5F3FF'); + bindCardClick('card-under-spec', '사양 부족 대상', p => p._spec_status === '사양 부족'); + bindCardClick('card-over-spec', '오버 스펙 대상', p => p._spec_status === '오버스펙'); + bindCardClick('card-win11-incompatible', '윈도우 11 업그레이드 불가 PC', p => isWindows11Incompatible(p.cpu, p.ram)); // 9. 조직별 사용 비율 집계 (전체 개인용 PC 기준) const deptCounts: Record = { @@ -822,36 +830,35 @@ function showMiniListModal(title: string, list: any[]) { display: flex; align-items: center; justify-content: center; - font-family: 'Pretendard', sans-serif; - color: #1E293B; + color: var(--text-main); `; modal.innerHTML = ` -
-
-

- +
+
+

+ ${title} 자산 목록 - ${list.length}대 + ${list.length}대

-
-

구분 (등급)보유량운영중재고부족분사양 적정성
${label}${data.total}대 (${totalRate}%)${data.active}대${data.stock}대${shortage}대 +
${label}${data.total}대 (${totalRate}%)${data.active}대${data.stock}대${shortage}대 ${barGraphHtml}
${label}${list.length}대
${label}${list.length}대
- - - - - - - +
사용자조직 (직무)주요 사양등급 (점수)자산코드
+ + + + + + + ${list.length === 0 - ? `` + ? `` : list.map(pc => { const spec = `${pc.cpu || ''} / ${pc.ram || ''} / ${pc.gpu || '-'}`; const user = pc.user_current || '(재고)'; @@ -862,12 +869,12 @@ function showMiniListModal(title: string, list: any[]) { const scoreHTML = `${score}점`; return ` - - - - + + + + - + `; }).join('') @@ -875,8 +882,8 @@ function showMiniListModal(title: string, list: any[]) {
사용자조직 (직무)주요 사양등급 (점수)자산코드
해당 등급의 자산이 없습니다.
해당 등급의 자산이 없습니다.
${user}${pc.current_dept || '-'} (${pc._resolved_position || pc.user_position || '-'})${spec}
${user}${pc.current_dept || '-'} (${pc._resolved_position || pc.user_position || '-'})${spec} ${badgeHTML}${scoreHTML}${pc.asset_code || '-'}${pc.asset_code || '-'}
-
-
@@ -970,10 +977,9 @@ function renderDonutChart(deptData: { label: string; count: number; color: strin top: 50%; left: 50%; transform: translate(-50%, -46%); - font-size: 1.65rem; + font-size: var(--fs-lg); font-weight: 900; - color: #1E5149; - font-family: 'Pretendard', sans-serif; + color: var(--primary); pointer-events: none; white-space: nowrap; `; diff --git a/src/views/List/ListFactory.ts b/src/views/List/ListFactory.ts index 8860635..8b7f02f 100644 --- a/src/views/List/ListFactory.ts +++ b/src/views/List/ListFactory.ts @@ -177,11 +177,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { } let currentFilters: any = (state as any).listFilters[filterKey]; - // 서버 탭이 아닐 경우 '자산 현황' 뷰 진입 방지 및 강제 'asset' 모드 (PC 탭은 자산 현황 숨김) const isServer = config.title === '서버'; - if (!isServer) { - (state as any).currentViewMode = 'asset'; - } // 1. 컨텐츠 영역 생성 (먼저 생성하여 참조 가능하게 함) const contentWrapper = document.createElement('div'); @@ -817,7 +813,8 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { const switchView = () => { contentWrapper.innerHTML = ''; - if ((state as any).currentViewMode === 'asset') { + const isAssetMode = !isServer || state.viewMode === 'list'; + if (isAssetMode) { filterBar.style.display = 'flex'; contentWrapper.style.overflowY = 'hidden'; contentWrapper.appendChild(tableWrapper); updateTable(); } else { @@ -834,7 +831,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { extraHTML: isServer ? `
@@ -878,13 +875,11 @@ export function createListView(container: HTMLElement, config: ListViewConfig) { const chkBox = filterBar.querySelector('#chk-list-view') as HTMLInputElement; const handleToggle = () => { - const isListMode = (state as any).currentViewMode === 'asset'; + const isListMode = chkBox.checked; if (isListMode) { - state.viewMode = 'location'; - (state as any).currentViewMode = 'location'; - } else { state.viewMode = 'list'; - (state as any).currentViewMode = 'asset'; + } else { + state.viewMode = 'location'; } window.dispatchEvent(new Event('refresh-view')); }; diff --git a/src/views/LocationView.ts b/src/views/LocationView.ts index cfa9cf7..1d6711b 100644 --- a/src/views/LocationView.ts +++ b/src/views/LocationView.ts @@ -163,15 +163,13 @@ export async function renderLocationView(container: HTMLElement) { const chkBox = container.querySelector('#chk-list-view-loc') as HTMLInputElement; if (chkBox) { - chkBox.checked = (state as any).currentViewMode === 'asset'; + chkBox.checked = state.viewMode === 'list'; const handleToggle = () => { const isListMode = chkBox.checked; if (isListMode) { state.viewMode = 'list'; - (state as any).currentViewMode = 'asset'; } else { state.viewMode = 'location'; - (state as any).currentViewMode = 'location'; } window.dispatchEvent(new Event('refresh-view')); }; diff --git a/src/views/MapEditor.ts b/src/views/MapEditor.ts index b050779..69a3841 100644 --- a/src/views/MapEditor.ts +++ b/src/views/MapEditor.ts @@ -250,8 +250,8 @@ export class MapEditor { #${i+1}
-
- ${optionsHtml}
diff --git a/vite.config.ts b/vite.config.ts index 56b1c1c..b5a8bda 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'vite'; +import { resolve } from 'path'; export default defineConfig({ server: { @@ -15,4 +16,12 @@ export default defineConfig({ } } }, + build: { + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html'), + map_editor: resolve(__dirname, 'map_editor.html'), + } + } + } });