feat: 대시보드 및 자산현황 레이아웃 개편 및 디자인 복원

- 자산현황 대시보드의 그래프를 제거하고 표와 상세정보 패널(5:5 비율)로 레이아웃 개편
- 표에서 '비고' 컬럼을 제거하고 '담당자(정)', '담당자(부)' 컬럼으로 교체 및 너비 조정
- 이중 필터(위치 -> 상세위치) 도입으로 필터링 기능 강화
- 상세정보 패널의 사진 영역을 Flexbox로 최적화하여 위아래 잘림 현상 원천 차단
- 모달창 내 '수정 모드' 폰트 색상을 디자인 가이드(var(--color-dahong))에 맞게 붉은 계열로 원상 복구 및 누락 변수 추가
- ListFactory.ts의 ASSET_SCHEMA.SERVICE_TYPE 참조 시 발생하던 TypeError 픽스
This commit is contained in:
2026-06-05 18:01:26 +09:00
parent eead43837d
commit 06f3baaa58
3 changed files with 85 additions and 65 deletions

BIN
backupDB_20260602.xlsx Normal file

Binary file not shown.

View File

@@ -32,6 +32,11 @@
--primary-hover: var(--primary-lv-5); --primary-hover: var(--primary-lv-5);
--primary-light: var(--primary-lv-0); --primary-light: var(--primary-lv-0);
--edit-mode-color: var(--color-dahong);
--edit-mode-light: rgba(255, 61, 0, 0.1);
--edit-mode-focus: rgba(255, 61, 0, 0.3);
--edit-mode-dark: #cc3100;
--text-main: #111827; --text-main: #111827;
--text-muted: #6B7280; --text-muted: #6B7280;
--border-color: #E5E7EB; --border-color: #E5E7EB;

View File

@@ -85,7 +85,8 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
fullList.forEach(asset => { fullList.forEach(asset => {
const loc = asset[ASSET_SCHEMA.LOCATION.key] || '미지정'; const loc = asset[ASSET_SCHEMA.LOCATION.key] || '미지정';
const serviceType = asset[ASSET_SCHEMA.SERVICE_TYPE.key] || '외부'; const serviceTypeKey = ASSET_SCHEMA.SERVICE_TYPE?.key || 'service_type';
const serviceType = asset[serviceTypeKey] || '외부';
const type = asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''; const type = asset[ASSET_SCHEMA.ASSET_TYPE.key] || '';
locationCounts[loc] = (locationCounts[loc] || 0) + 1; locationCounts[loc] = (locationCounts[loc] || 0) + 1;
@@ -138,14 +139,17 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
? `<tr><td colspan="4" style="padding: 3rem; text-align: center; color: var(--text-muted);">조회된 자산이 없습니다.</td></tr>` ? `<tr><td colspan="4" style="padding: 3rem; text-align: center; color: var(--text-muted);">조회된 자산이 없습니다.</td></tr>`
: finalDisplayList.map(asset => { : finalDisplayList.map(asset => {
const purpose = asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || ''; const purpose = asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || '';
const serviceType = asset[ASSET_SCHEMA.SERVICE_TYPE.key] || '외부'; const serviceTypeKey = ASSET_SCHEMA.SERVICE_TYPE?.key || 'service_type';
const serviceType = asset[serviceTypeKey] || '외부';
const labelColor = serviceType === '내부' ? '#94A3B8' : '#35635C'; const labelColor = serviceType === '내부' ? '#94A3B8' : '#35635C';
const memo = asset[ASSET_SCHEMA.MEMO.key] || ''; const managerMain = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-';
const managerSub = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '-';
return ` return `
<tr style="border-bottom: 1px solid var(--border-color); cursor: pointer;" class="mini-row" data-id="${asset.id}"> <tr style="border-bottom: 1px solid var(--border-color); cursor: pointer;" class="mini-row" data-id="${asset.id}">
<td style="padding: 10px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><span style="color: ${labelColor}; font-weight: 700; font-size: 12px;">${serviceType}</span></td> <td style="padding: 10px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><span style="color: ${labelColor}; font-weight: 700; font-size: 12px;">${serviceType}</span></td>
<td style="padding: 10px 0; font-weight: 600; color: var(--text-main); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${purpose}">${purpose || '-'}</td> <td style="padding: 10px 0; font-weight: 600; color: var(--text-main); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${purpose}">${purpose || '-'}</td>
<td style="padding: 10px 0; color: var(--text-muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${formatInline(memo)}">${formatInline(memo) || '-'}</td> <td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${managerMain}</td>
<td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${managerSub}</td>
<td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td> <td style="padding: 10px 0; text-align: center; color: var(--text-main); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'}</td>
</tr>`; </tr>`;
}).join(''); }).join('');
@@ -207,50 +211,76 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
</div> </div>
</div> </div>
<div style="display: grid; grid-template-columns: 1.2fr 1.6fr; gap: 2.5rem; flex: 1; min-height: 0;"> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; flex: 1; min-height: 0;">
<!-- 차트 구역 --> <!-- 좌측: 자산 현황 표 (5:5 비율) -->
<div class="chart-section" style="display: flex; flex-direction: column; min-height: 0; width: 100%;"> <div class="list-section" style="display: flex; flex-direction: column; min-height: 0; background: white; border-radius: 8px; border: 1px solid var(--border-color); overflow: hidden;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; flex-shrink: 0;"> <div style="display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1.25rem; background: #f9fafb; border-bottom: 1px solid var(--border-color); flex-shrink: 0;">
<h4 style="font-size: 14px; font-weight: 700; color: var(--text-main);">${isPcView ? '유형별 분포' : '위치별 분포'}</h4> <h4 id="list-section-title" style="font-size: 14px; font-weight: 700; color: var(--text-main); margin:0;">자산 현황 목록</h4>
${!isPcView && selectedLocation ? `<button id="btn-reset-loc" style="font-size:11px; color:var(--primary-color); background:none; border:none; cursor:pointer; font-weight:700;">초기화 ↺</button>` : ''} <div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 11px; font-weight: 600; color: var(--text-muted);">위치:</span>
<select id="select-loc" style="padding: 2px 8px; font-size: 11px; border-radius: 4px; border: 1px solid var(--border-color); outline: none; background: white; cursor:pointer; font-family: 'Pretendard';">
<option value="">전체</option>
${Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key] || '미지정'))).sort().map(l => `<option value="${l}" ${l === selectedLocation ? 'selected' : ''}>${l}</option>`).join('')}
</select>
<span style="font-size: 11px; font-weight: 600; color: var(--text-muted);">상세:</span>
<select id="select-detail-loc" style="padding: 2px 8px; font-size: 11px; border-radius: 4px; border: 1px solid var(--border-color); outline: none; background: white; cursor:pointer; font-family: 'Pretendard'; max-width: 120px;"></select>
</div> </div>
<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 1rem; flex-shrink: 0;">
${chartLabels.map((l, i) => `
<div style="font-size: 11px; display: flex; align-items: center; gap: 5px; cursor:pointer;
color: ${!isPcView && selectedLocation === l ? 'var(--primary-color)' : 'var(--text-main)'};
font-weight: ${!isPcView && selectedLocation === l ? '800' : '500'};"
${!isPcView ? `onclick="window.dispatchLocFilter('${l}')"` : ''}>
<span style="width: 6px; height: 6px; border-radius: 50%; background: ${chartColors[i % chartColors.length]}; opacity: ${!isPcView && selectedLocation && selectedLocation !== l ? 0.2 : 0.8}"></span>
<span>${l}</span>
<span style="color: var(--text-muted); opacity: 0.6;">${chartData[i]}</span>
</div> </div>
`).join('')} <div style="flex: 1; overflow-y: auto;">
</div> <table style="width: 100%; border-collapse: collapse; table-layout: fixed;">
<div style="flex: 1; position: relative; min-height: 250px; max-height: 320px; display: flex; justify-content: center;"> <thead style="position: sticky; top: 0; background: #fefefe; z-index: 10;">
<canvas id="system-location-chart"></canvas> <tr style="text-align: left; font-size: 11px; color: var(--text-muted);">
<th style="padding: 10px 0; font-weight: 700; border-bottom: 1px solid var(--border-color); width: 60px; text-align:center;">분류</th>
<th style="padding: 10px 0; font-weight: 700; border-bottom: 1px solid var(--border-color); width: 130px;">용도/자산명</th>
<th style="padding: 10px 0; font-weight: 700; border-bottom: 1px solid var(--border-color); text-align:center; width: 90px;">관리자(정)</th>
<th style="padding: 10px 0; font-weight: 700; border-bottom: 1px solid var(--border-color); text-align:center; width: 90px;">관리자(부)</th>
<th style="padding: 10px 0; text-align: center; font-weight: 700; border-bottom: 1px solid var(--border-color); width: 100px;">상세위치</th>
</tr>
</thead>
<tbody id="system-status-tbody" style="font-size: 12px;"></tbody>
</table>
</div> </div>
</div> </div>
<div class="list-section" style="display: flex; flex-direction: column; min-height: 0;"> <!-- 우측: 상세 정보 패널 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; height: 32px; flex-shrink: 0;"> <div id="system-detail-panel" style="display: flex; flex-direction: column; min-height: 0; background: #fcfcfc; border-radius: 8px; border: 1px solid var(--border-color); padding: 1.5rem; overflow: hidden; box-shadow: 0 10px 25px rgba(0,0,0,0.05);">
<h4 id="list-section-title" style="font-size: 14px; font-weight: 700; color: var(--text-main);">자산등록현황</h4> <div id="detail-empty-state" style="height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--text-muted); text-align: center;">
<div style="display: flex; align-items: center; gap: 8px;"> <div style="font-size: 4rem; margin-bottom: 1.5rem; opacity: 0.15;">🖼️</div>
<span style="font-size: 11px; font-weight: 600; color: var(--text-muted);">상세위치 필터:</span> <p style="font-size: 1.125rem; font-weight: 500;">목록에서 자산을 선택하면<br><strong>상세 정보와 배치도</strong>가 이곳에 표시됩니다.</p>
<select id="select-detail-loc" style="padding: 2px 8px; font-size: 11px; border-radius: 4px; border: 1px solid var(--border-color); outline: none; background: white; cursor:pointer; font-family: 'Pretendard';"></select> </div>
<div id="detail-content" style="display: none; height: 100%; flex-direction: column;">
<!-- 상단 요약 정보 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.25rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-color); flex-shrink: 0;">
<div style="display: flex; gap: 2rem; align-items: center;">
<div>
<label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 2px;">자산번호</label>
<div id="detail-asset-code" style="font-size: 1.25rem; font-weight: 800; color: var(--primary-color);"></div>
</div>
<div>
<label style="display: block; font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 2px;">메모 요약</label>
<div id="detail-memo" style="font-size: 13px; color: var(--text-main); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 300px;"></div>
</div>
</div>
<button id="btn-system-view-detail" class="btn-primary" style="padding: 0.5rem 1.25rem; font-size: 12px; font-weight: 600;">상세수정</button>
</div>
<!-- 메인 배치도 영역 (잘림 방지 보정) -->
<div style="flex: 1; display: flex; flex-direction: column; min-height: 0; margin-top: 0.5rem; overflow: hidden;">
<div style="margin-bottom: 0.5rem; flex-shrink: 0;">
<label style="font-size: 11px; font-weight: 700; color: var(--text-main); text-transform: uppercase;">자산 위치 배치도 / 사진</label>
</div>
<div id="detail-photo-wrapper" style="width: 100%; flex: 1; background: #1a1a1a; border-radius: 6px; overflow: hidden; display: flex; align-items: center; justify-content: center; border: 1px solid #000; box-shadow: inset 0 0 50px rgba(0,0,0,0.6); position: relative;">
<div class="layout-map-container readonly" style="position: relative; display: inline-block; line-height: 0; max-width: 100%; max-height: 100%;">
<img id="detail-photo" src="" style="display: block; max-width: 100%; max-height: 100%; object-fit: contain;" />
<div id="detail-marker" class="layout-marker pulse-marker" style="display: none; position: absolute;"></div>
<div id="detail-overlay-layer" class="digital-overlay-layer" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;"></div>
</div>
<div id="detail-no-photo" style="display: none; flex-direction: column; align-items: center; gap: 1rem;">
<div style="font-size: 4rem; opacity: 0.15;">🖼️</div>
<span style="color: #555; font-size: 13px; font-weight: 500;">등록된 배치도 또는 사진이 없습니다.</span>
</div>
</div> </div>
</div> </div>
<div style="flex: 1; overflow-y: auto; border-top: 2px solid var(--text-main);">
<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">
<thead style="position: sticky; top: 0; background: white; z-index: 10;">
<tr style="text-align: left; font-size: 11px;">
<th style="padding: 8px 0; font-weight: 700; border-bottom: 1px solid var(--border-color); width: 50px;">분류</th>
<th style="padding: 8px 0; font-weight: 700; border-bottom: 1px solid var(--border-color); width: 130px;">용도</th>
<th style="padding: 8px 0; font-weight: 700; border-bottom: 1px solid var(--border-color);">비고</th>
<th style="padding: 8px 0; text-align: center; font-weight: 700; border-bottom: 1px solid var(--border-color); width: 90px;">상세위치</th>
</tr>
</thead>
<tbody id="system-status-tbody"></tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
@@ -258,42 +288,27 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
`; `;
(window as any).dispatchLocFilter = (loc: string) => { (window as any).dispatchLocFilter = (loc: string) => {
if (isPcView) return; // PC 뷰에서는 위치 필터링 비활성화 (유형별로 보기 때문) if (isPcView) return;
selectedLocation = loc; selectedLocation = loc;
selectedDetailLocation = null; selectedDetailLocation = null;
renderSystemStatus(); renderSystemStatus();
}; };
setTimeout(() => { setTimeout(() => {
const ctx = document.getElementById('system-location-chart') as HTMLCanvasElement; const selectLoc = document.getElementById('select-loc') as HTMLSelectElement;
if (ctx && typeof (window as any).Chart !== 'undefined') { const selectDetailLoc = document.getElementById('select-detail-loc') as HTMLSelectElement;
new (window as any).Chart(ctx, {
type: 'doughnut', selectLoc?.addEventListener('change', (e) => {
data: { labels: chartLabels, datasets: [{ data: chartData, backgroundColor: chartColors, borderWidth: 0 }] }, selectedLocation = (e.target as HTMLSelectElement).value || null;
options: {
responsive: true, maintainAspectRatio: false, cutout: '70%',
onClick: (evt: any, elements: any[]) => {
if (!isPcView && elements.length > 0) {
selectedLocation = locLabels[elements[0].index];
selectedDetailLocation = null; selectedDetailLocation = null;
renderSystemStatus(); updateTableOnly();
}
},
plugins: { legend: { display: false } }
}
}); });
} selectDetailLoc?.addEventListener('change', (e) => {
document.getElementById('btn-reset-loc')?.addEventListener('click', () => {
selectedLocation = null;
selectedDetailLocation = null;
renderSystemStatus();
});
document.getElementById('select-detail-loc')?.addEventListener('change', (e) => {
selectedDetailLocation = (e.target as HTMLSelectElement).value || null; selectedDetailLocation = (e.target as HTMLSelectElement).value || null;
updateTableOnly(); updateTableOnly();
}); });
updateTableOnly(); updateTableOnly();
}, 100); }, 50);
}; };
// [자산 목록] 테이블 렌더러 // [자산 목록] 테이블 렌더러