feat: 대시보드 및 자산현황 레이아웃 개편 및 디자인 복원
- 자산현황 대시보드의 그래프를 제거하고 표와 상세정보 패널(5:5 비율)로 레이아웃 개편 - 표에서 '비고' 컬럼을 제거하고 '담당자(정)', '담당자(부)' 컬럼으로 교체 및 너비 조정 - 이중 필터(위치 -> 상세위치) 도입으로 필터링 기능 강화 - 상세정보 패널의 사진 영역을 Flexbox로 최적화하여 위아래 잘림 현상 원천 차단 - 모달창 내 '수정 모드' 폰트 색상을 디자인 가이드(var(--color-dahong))에 맞게 붉은 계열로 원상 복구 및 누락 변수 추가 - ListFactory.ts의 ASSET_SCHEMA.SERVICE_TYPE 참조 시 발생하던 TypeError 픽스
This commit is contained in:
BIN
backupDB_20260602.xlsx
Normal file
BIN
backupDB_20260602.xlsx
Normal file
Binary file not shown.
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
// [자산 목록] 테이블 렌더러
|
// [자산 목록] 테이블 렌더러
|
||||||
|
|||||||
Reference in New Issue
Block a user