import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema'; import { dynamicSort, renderPageHeader, calculateAssetAge, formatInline, isWindows11Incompatible, calculatePcScoreDeductive } from '../../core/utils'; import { setupTableSorting, SortState } from '../../core/tableHandler'; import { renderFilterBar, applyCommonFilters } from '../../core/filterHandler'; import { state } from '../../core/state'; import { IMAGE_LOCATIONS } from '../../components/Modal/SharedData'; import './table.css'; declare var Chart: any; let pcFlowChartInstance: any = null; export interface ColumnDef { header: string; sortKey?: string; width?: string; align?: 'left' | 'center' | 'right'; className?: string; render: (asset: any) => string; } export interface ListViewConfig { title: string; dataSource: () => any[]; searchKeys: string[]; filterOptions: { keywordLabel: string; showCorp?: boolean; showDept?: boolean; showLoc?: boolean; showField?: boolean; showType?: boolean; showStatus?: boolean; showPosition?: boolean; }; columns: ColumnDef[]; onRowClick?: (asset: any) => void; emptyMessage?: string; persistentSortState?: SortState; } export function createListView(container: HTMLElement, config: ListViewConfig) { // 1. 컨테이너 초기화 container.innerHTML = ''; const fullList = config.dataSource(); let sortState: SortState = config.persistentSortState || { key: '', direction: 'asc' }; if (!(state as any).listFilters) { (state as any).listFilters = {}; } const filterKey = config.title; if (!(state as any).listFilters[filterKey]) { (state as any).listFilters[filterKey] = { keyword: '', corp: '', dept: '', loc: '', field: '', type: '', status: '' }; } let currentFilters: any = (state as any).listFilters[filterKey]; const isServer = config.title === '서버'; // 1. 컨텐츠 영역 생성 (먼저 생성하여 참조 가능하게 함) const contentWrapper = document.createElement('div'); contentWrapper.className = 'view-content-wrapper'; // 2. 필터 바 생성 (자산 목록에서만 사용) const filterBar = document.createElement('div'); filterBar.className = 'search-bar'; // 자산 추가 버튼 및 목록 보기 체크박스 추가 로직 const showPcFlowBtn = config.title === 'PC'; container.appendChild(filterBar); container.appendChild(contentWrapper); // --- 내부 상태 --- let selectedLocation: string | null = null; let selectedDetailLocation: string | null = null; let dynamicMapConfig: Record = {}; const fetchMapConfig = async () => { try { const res = await fetch(`http://${location.hostname}:3000/api/maps`); dynamicMapConfig = await res.json(); } catch (err) { console.error('Failed to fetch map config:', err); } }; fetchMapConfig(); // [자산 현황] 대시보드 렌더러 const renderSystemStatus = () => { const isPcView = config.title === 'PC'; // 실제 보유 자산이 존재하는 위치 목록만 추출 (0대인 곳 제외) const validLocations = Array.from(new Set(fullList.map(a => a[ASSET_SCHEMA.LOCATION.key] || '미지정'))) .filter(l => { const count = fullList.filter(a => (a[ASSET_SCHEMA.LOCATION.key] || '미지정') === l).length; return count > 0; }) .sort(); // 초기값이나 유효하지 않은 값이 지정되어 있다면 첫 번째 유효 위치로 동적 갱신 if (!selectedLocation || !validLocations.includes(selectedLocation)) { selectedLocation = validLocations[0] || ''; } // 동적 통계 수집 객체 const extStats = { total: 0, locCounts: {} as Record, typeCounts: {} as Record, typeLocMap: {} as Record>, locWarning: 0, typeWarning: 0 }; const intStats = { total: 0, locCounts: {} as Record, typeCounts: {} as Record, typeLocMap: {} as Record> }; const checkAnomaly = (serviceType: string, loc: string, type: string) => { if (serviceType !== '외부') return { isWarning: false, isLocWarning: false, isTypeWarning: false }; const isLocWarning = loc !== 'IDC' && loc !== '미지정' && loc !== ''; const isTypeWarning = type.toLowerCase().replace(/\s/g, '').includes('서버pc'); return { isWarning: isLocWarning || isTypeWarning, isLocWarning, isTypeWarning }; }; fullList.forEach(asset => { const loc = asset[ASSET_SCHEMA.LOCATION.key] || '미지정'; const serviceType = asset.service_type || '외부'; const type = asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''; const targetStat = serviceType === '내부' ? intStats : extStats; targetStat.total++; if (loc) targetStat.locCounts[loc] = (targetStat.locCounts[loc] || 0) + 1; if (type) { targetStat.typeCounts[type] = (targetStat.typeCounts[type] || 0) + 1; if (!targetStat.typeLocMap[type]) targetStat.typeLocMap[type] = {}; targetStat.typeLocMap[type][loc] = (targetStat.typeLocMap[type][loc] || 0) + 1; } if (serviceType === '외부') { const anomaly = checkAnomaly(serviceType, loc, type); if (anomaly.isLocWarning) extStats.locWarning++; if (anomaly.isTypeWarning) extStats.typeWarning++; } }); const generateDetailStatHTML = (title: string, stats: any) => `
${title}
${stats.locWarning ? `위치부적절: ${stats.locWarning}` : ''} ${stats.typeWarning ? `형식부적절: ${stats.typeWarning}` : ''}
${Object.entries(stats.locCounts as Record).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([l, c]) => `${l}: ${c}`).join('')}
${Object.entries(stats.typeCounts as Record).sort((a, b) => b[1] - a[1]).slice(0, 6).map(([t, c]) => { const locDist = stats.typeLocMap[t] || {}; const locHint = Object.entries(locDist).sort((a: any, b: any) => b[1] - a[1]).map(([l, count]) => `${l}: ${count}대`).join('\n'); return `${t}: ${c}`; }).join('')}
`; contentWrapper.innerHTML = `
총 보유 자산
${fullList.length}
외부: ${extStats.total} 내부: ${intStats.total}
${generateDetailStatHTML('외부 (운영) 상세', extStats)}
${generateDetailStatHTML('내부 (테스트) 상세', intStats)}
${!isPcView ? `
위치:
` : ''}
${isPcView ? ` ` : ` `}
일자 담당자 구분 사용자 인수자 자산번호 상세
분류 용도/자산명 관리자(정) 관리자(부) 상세위치
${isPcView ? `
사용자 부서 (직무) 상태 자산코드
사양 주의 자산이 없습니다.
` : `

목록에서 자산을 선택하면
상세 정보와 배치도가 표시됩니다.

`}
`; const updateFlowLogsSection = () => { if (!isPcView) return; // 사양 주의 장비 현황 (부족/오버스펙) 계산 및 바인딩 const specMismatchTbody = document.getElementById('spec-mismatch-tbody'); if (specMismatchTbody) { // fullList 중 개인 PC 관련 장비 필터링 const pcs = fullList.filter((a: any) => { const type = a[ASSET_SCHEMA.ASSET_TYPE.key] || ''; const job = a[ASSET_SCHEMA.USER_POSITION.key] || ''; const status = a[ASSET_SCHEMA.HW_STATUS.key] || ''; const user = a[ASSET_SCHEMA.CURRENT_USER.key] || ''; // 운영 중이고 사용자가 할당되어 있으며, 직무가 재고PC가 아닌 실사용 기기 대상 return job !== '재고PC' && status === '운영' && user.trim() !== ''; }); // 직무별 평균 점수 산출 const jobScores: Record = {}; pcs.forEach((pc: any) => { const job = pc[ASSET_SCHEMA.USER_POSITION.key] || '미분류'; const cpu = pc[ASSET_SCHEMA.CPU.key] || ''; const ram = pc[ASSET_SCHEMA.RAM.key] || ''; const gpu = pc[ASSET_SCHEMA.GPU.key] || ''; const pDate = pc[ASSET_SCHEMA.PURCHASE_DATE.key] || ''; const score = calculatePcScoreDeductive(cpu, ram, gpu, pDate); pc['_pc_score'] = score; if (!jobScores[job]) jobScores[job] = { totalScore: 0, count: 0, avg: 0 }; jobScores[job].totalScore += score; jobScores[job].count += 1; }); Object.keys(jobScores).forEach(job => { jobScores[job].avg = jobScores[job].count > 0 ? jobScores[job].totalScore / jobScores[job].count : 0; }); // DB 기준 사양 데이터 맵핑 (state.masterData.jobSpecs 이용) const jobSpecsMap: Record = {}; if (state.masterData.jobSpecs) { state.masterData.jobSpecs.forEach((s: any) => { jobSpecsMap[s.job_name] = s.required_grade || '중급'; }); } // 사용자 이름 → 세부 직무 맵 생성 (system_users.position 기준) const userPositionMap: Record = {}; if (state.masterData.users) { state.masterData.users.forEach((u: any) => { if (u.user_name && u.position) { userPositionMap[u.user_name.trim()] = u.position.trim(); } }); } const GRADE_RANK: Record = { 'premium': 4, '최상급': 4, 'high': 3, '상급': 3, 'normal': 2, '중급': 2, 'entry': 1, '보급': 1, 'replace': 0, '교체 대상': 0 }; // 기준 대비 사양 부족/오버스펙 분류 const criticalPcList: any[] = []; pcs.forEach((pc: any) => { const userName = (pc[ASSET_SCHEMA.CURRENT_USER.key] || '').trim(); const job = userPositionMap[userName] || pc[ASSET_SCHEMA.USER_POSITION.key] || '미분류'; const score = pc['_pc_score']; const requiredGrade = jobSpecsMap[job] || jobSpecsMap[pc[ASSET_SCHEMA.USER_POSITION.key]] || '중급'; const cpu = pc[ASSET_SCHEMA.CPU.key] || ''; const ram = pc[ASSET_SCHEMA.RAM.key] || ''; const win11Incompatible = isWindows11Incompatible(cpu, ram); let actualGrade = 'replace'; if (score >= 85) actualGrade = 'premium'; else if (score >= 70) actualGrade = 'high'; else if (score >= 40) actualGrade = 'normal'; else if (score >= 20) actualGrade = 'entry'; const reqRank = GRADE_RANK[requiredGrade] !== undefined ? GRADE_RANK[requiredGrade] : 2; const actRank = GRADE_RANK[actualGrade] !== undefined ? GRADE_RANK[actualGrade] : 0; let isUnder = false; if (job !== '재고PC') { if (win11Incompatible) { isUnder = true; pc['_spec_status'] = '사양 부족'; } else if (actRank < reqRank) { isUnder = true; pc['_spec_status'] = '사양 부족'; } else if (actRank > reqRank) { pc['_spec_status'] = '오버스펙'; criticalPcList.push(pc); } else { pc['_spec_status'] = '적정'; } } else { if (win11Incompatible) { isUnder = true; pc['_spec_status'] = '사양 부족'; } else { pc['_spec_status'] = '적정'; } } if (isUnder) { criticalPcList.push(pc); } }); // 정렬: 요구 등급 대비 실제 성능이 많이 부족한 순(등급 편차가 큰 순)으로 정렬 criticalPcList.sort((a: any, b: any) => { const userNameA = (a[ASSET_SCHEMA.CURRENT_USER.key] || '').trim(); const userNameB = (b[ASSET_SCHEMA.CURRENT_USER.key] || '').trim(); const jobA = userPositionMap[userNameA] || a[ASSET_SCHEMA.USER_POSITION.key] || '미분류'; const jobB = userPositionMap[userNameB] || b[ASSET_SCHEMA.USER_POSITION.key] || '미분류'; const reqA = jobSpecsMap[jobA] || jobSpecsMap[a[ASSET_SCHEMA.USER_POSITION.key]] || '중급'; const reqB = jobSpecsMap[jobB] || jobSpecsMap[b[ASSET_SCHEMA.USER_POSITION.key]] || '중급'; const scoreA = a['_pc_score']; const scoreB = b['_pc_score']; let actA = 'replace'; if (scoreA >= 85) actA = 'premium'; else if (scoreA >= 70) actA = 'high'; else if (scoreA >= 40) actA = 'normal'; else if (scoreA >= 20) actA = 'entry'; let actB = 'replace'; if (scoreB >= 85) actB = 'premium'; else if (scoreB >= 70) actB = 'high'; else if (scoreB >= 40) actB = 'normal'; else if (scoreB >= 20) actB = 'entry'; const devA = (GRADE_RANK[reqA] || 2) - (GRADE_RANK[actA] || 0); const devB = (GRADE_RANK[reqB] || 2) - (GRADE_RANK[actB] || 0); if (devA !== devB) { return devB - devA; // 편차가 큰 것(더 많이 부족한 것)이 먼저 정렬됨 } return scoreA - scoreB; // 편차가 같으면 성능 점수가 낮은 순 }); if (criticalPcList.length === 0) { specMismatchTbody.innerHTML = '사양 주의 자산이 없습니다.'; } else { specMismatchTbody.innerHTML = criticalPcList.map((pc: any) => { const user = pc[ASSET_SCHEMA.CURRENT_USER.key] || '-'; const dept = pc[ASSET_SCHEMA.CURRENT_DEPT.key] || '-'; const job = pc[ASSET_SCHEMA.USER_POSITION.key] || '-'; const status = pc['_spec_status']; const assetCode = pc.asset_code || '-'; const badgeColor = status === '사양 부족' ? 'background:#FFF1F2; color:#E11D48; border: 1px solid #FDA4AF;' : 'background:#F0FDF4; color:#16A34A; border: 1px solid #BBF7D0;'; return ` ${user} ${dept} (${job}) ${status === '오버스펙' ? '오버 스펙' : status} ${assetCode} `; }).join(''); // 클릭 시 해당 자산 상세 페이지로 전환 specMismatchTbody.querySelectorAll('.spec-row').forEach(row => { row.addEventListener('click', () => { specMismatchTbody.querySelectorAll('.spec-row').forEach(r => { (r as HTMLElement).style.backgroundColor = 'transparent'; }); (row as HTMLElement).style.backgroundColor = '#EBF2F1'; // 선택 하이라이트 const assetId = row.getAttribute('data-id'); const found = fullList.find(a => String(a.id) === String(assetId)); if (found) { updateDetailPanel(found); } }); }); } } }; const updateDetailPanel = (asset: any) => { const emptyState = document.getElementById('detail-empty-state'); const content = document.getElementById('detail-content'); if (!emptyState || !content) return; emptyState.classList.add('hidden'); content.classList.remove('hidden'); const codeEl = document.getElementById('detail-asset-code'); const typeEl = document.getElementById('detail-asset-type'); const viewBtn = document.getElementById('btn-view-full-detail') as HTMLButtonElement; if (codeEl) codeEl.textContent = asset.asset_code || '미지정'; if (typeEl) typeEl.textContent = asset.asset_type || '-'; if (viewBtn) viewBtn.onclick = () => config.onRowClick && config.onRowClick(asset); const photo = document.getElementById('detail-photo') as HTMLImageElement; const marker = document.getElementById('detail-marker'); const overlayLayer = document.getElementById('detail-overlay-layer'); const noPhoto = document.getElementById('detail-no-photo'); const bldg = asset.location || ''; const detail = asset.location_detail || ''; const x = asset.loc_x; const y = asset.loc_y; const hasCoords = (x !== null && x !== undefined && x !== '' && x !== 'null'); const savedImg = asset.location_photo || asset.loc_img; const locImgs = IMAGE_LOCATIONS[bldg.trim()]?.[detail.trim()] || null; const imgPath = (savedImg && locImgs?.includes(savedImg)) ? savedImg : (locImgs ? locImgs[0] : null); const htmlMap = document.getElementById('detail-html-map') as HTMLIFrameElement; const isHtmlMap = imgPath?.toLowerCase().endsWith('.html'); if (imgPath && hasCoords) { if (isHtmlMap) { photo.style.display = 'none'; if (marker) marker.style.display = 'none'; if (overlayLayer) overlayLayer.innerHTML = ''; if (htmlMap) { htmlMap.src = `${imgPath}?markerX=${x}&markerY=${y}`; htmlMap.classList.remove('hidden'); htmlMap.style.display = 'block'; } } else { if (htmlMap) { htmlMap.src = ''; htmlMap.classList.add('hidden'); htmlMap.style.display = 'none'; } photo.src = imgPath; photo.style.display = 'block'; } if (noPhoto) noPhoto.style.display = 'none'; if (!isHtmlMap) { photo.onload = () => { const updateMarkerPos = () => { const imgW = photo.clientWidth; const imgH = photo.clientHeight; if (marker) { marker.style.left = `calc(50% - ${imgW/2}px + ${ (parseFloat(x as string) * imgW) / 100 }px)`; marker.style.top = `calc(50% - ${imgH/2}px + ${ (parseFloat(y as string) * imgH) / 100 }px)`; marker.style.display = 'block'; } if (overlayLayer) { overlayLayer.style.width = `${imgW}px`; overlayLayer.style.height = `${imgH}px`; overlayLayer.style.left = `calc(50% - ${imgW/2}px)`; overlayLayer.style.top = `calc(50% - ${imgH/2}px)`; const boxes = dynamicMapConfig[imgPath] || []; if (boxes.length > 0) { overlayLayer.innerHTML = ` ${boxes.map((b, i) => { const isSelected = b.x === x && b.y === y; const fill = isSelected ? 'rgba(255, 61, 0, 0.4)' : 'rgba(30, 81, 73, 0.02)'; const stroke = isSelected ? '#FF3D00' : 'rgba(30, 81, 73, 0.15)'; const strokeWidth = isSelected ? '0.8' : '0.2'; if (isSelected && marker) { marker.style.left = `calc(50% - ${imgW/2}px + ${ (parseFloat(b.x) + parseFloat(b.w)/2) * imgW / 100 }px)`; marker.style.top = `calc(50% - ${imgH/2}px + ${ (parseFloat(b.y) + parseFloat(b.h)/2) * imgH / 100 }px)`; } return ``; }).join('')} `; } else { overlayLayer.innerHTML = ''; } } }; updateMarkerPos(); window.addEventListener('resize', updateMarkerPos); }; } } else { photo.style.display = 'none'; if (htmlMap) { htmlMap.src = ''; htmlMap.style.display = 'none'; } if (marker) marker.style.display = 'none'; if (overlayLayer) overlayLayer.innerHTML = ''; if (noPhoto) { noPhoto.classList.remove('hidden'); noPhoto.style.display = 'flex'; } } }; // [자산 현황] 테이블 렌더러 const updateTableOnly = () => { let filtered = selectedLocation ? fullList.filter(a => (a[ASSET_SCHEMA.LOCATION.key] || '미지정') === selectedLocation) : fullList; const currentDetailLocs = Array.from(new Set(filtered.map(a => a[ASSET_SCHEMA.LOC_DETAIL.key] || '미지정'))).sort(); if (selectedDetailLocation) filtered = filtered.filter(a => (a[ASSET_SCHEMA.LOC_DETAIL.key] || '미지정') === selectedDetailLocation); const finalDisplayList = (!selectedLocation && !selectedDetailLocation) ? filtered.slice(0, 20) : filtered; const titleEl = document.getElementById('list-section-title'); if (titleEl) titleEl.textContent = selectedLocation ? `${selectedLocation} 자산 현황 (${finalDisplayList.length}대)` : '위치별 자산등록현황 (최근 등록)'; const selectEl = document.getElementById('select-detail-loc') as HTMLSelectElement; if (selectEl && !selectedDetailLocation) { selectEl.innerHTML = `` + currentDetailLocs.map(dl => ``).join(''); } const tbody = document.getElementById('system-status-tbody'); if (tbody) { tbody.innerHTML = finalDisplayList.length === 0 ? `조회된 자산이 없습니다.` : finalDisplayList.map(asset => { const purpose = asset[ASSET_SCHEMA.ASSET_PURPOSE.key] || ''; const serviceType = asset.service_type || '외부'; const type = asset[ASSET_SCHEMA.ASSET_TYPE.key] || ''; const loc = asset[ASSET_SCHEMA.LOCATION.key] || ''; const isWarning = serviceType === '외부' && (loc !== 'IDC' || type.toLowerCase().includes('서버pc')); const managerMain = asset[ASSET_SCHEMA.MANAGER_MAIN.key] || '-'; const managerSub = asset[ASSET_SCHEMA.MANAGER_SUB.key] || '-'; return ` ${serviceType} ${purpose || '-'} ${managerMain} ${managerSub} ${asset[ASSET_SCHEMA.LOC_DETAIL.key] || '-'} `; }).join(''); tbody.querySelectorAll('.mini-row').forEach(row => { row.addEventListener('click', () => { tbody.querySelectorAll('.mini-row').forEach(r => r.classList.remove('active')); row.classList.add('active'); const asset = fullList.find(a => a.id === row.getAttribute('data-id')); if (asset) updateDetailPanel(asset); }); }); } }; setTimeout(() => { document.getElementById('select-loc')?.addEventListener('change', (e) => { selectedLocation = (e.target as HTMLSelectElement).value || null; selectedDetailLocation = null; updateTableOnly(); }); document.getElementById('select-detail-loc')?.addEventListener('change', (e) => { selectedDetailLocation = (e.target as HTMLSelectElement).value || null; updateTableOnly(); }); updateTableOnly(); updateFlowLogsSection(); }, 50); }; const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-container'; const table = document.createElement('table'); const thead = document.createElement('thead'); const tbody = document.createElement('tbody'); table.appendChild(thead); table.appendChild(tbody); tableWrapper.appendChild(table); const updateTable = () => { let filtered = applyCommonFilters(fullList, currentFilters, config.searchKeys as any[]); if (sortState.key) filtered = dynamicSort(filtered, sortState.key, sortState.direction); thead.innerHTML = `${config.columns.map(col => { const isDateCol = col.header.includes('일') || col.header.includes('날짜') || col.header.includes('연월'); const alignmentClass = col.align ? `text-${col.align}` : (isDateCol ? 'text-center' : ''); return `${col.header}`; }).join('')}`; tbody.innerHTML = filtered.length === 0 ? `${UI_TEXT.MESSAGES.NO_DATA}` : filtered.map(asset => `${config.columns.map(col => { const isDateCol = col.header.includes('일') || col.header.includes('날짜') || col.header.includes('연월'); const alignmentClass = col.align ? `text-${col.align}` : (isDateCol ? 'text-center' : ''); const customClass = col.className || ''; const rendered = col.render(asset); const rawText = rendered.replace(/<[^>]*>/g, '').trim(); const titleAttr = rawText && rawText !== '-' ? `title="${rawText.replace(/"/g, '"')}"` : ''; return `${rendered}`; }).join('')}`).join(''); tbody.querySelectorAll('.asset-row').forEach((tr, idx) => { tr.addEventListener('click', () => config.onRowClick && config.onRowClick(filtered[idx])); }); setupTableSorting(table, sortState, (key, dir) => { sortState = { key, direction: dir }; updateTable(); }); }; const switchView = () => { contentWrapper.innerHTML = ''; const isAssetMode = !isServer || state.viewMode === 'list'; if (isAssetMode) { filterBar.style.display = 'flex'; contentWrapper.style.overflowY = 'hidden'; contentWrapper.appendChild(tableWrapper); updateTable(); } else { filterBar.style.display = 'none'; contentWrapper.style.overflowY = 'hidden'; renderSystemStatus(); } }; // 2. 필터 바 렌더링 renderFilterBar(filterBar, { ...config.filterOptions, initialFilters: currentFilters, fullList: fullList, // Added for dynamic options extraHTML: isServer ? `
` : '', onFilterChange: (filters) => { Object.assign(currentFilters, filters); updateTable(); } }); // 3. 필터 바 내 액션 버튼 배치 const actionContainer = filterBar.querySelector('#filter-bar-actions'); if (actionContainer) { actionContainer.className = "header-action-group"; actionContainer.innerHTML = ` ${showPcFlowBtn ? ` ` : ''} `; actionContainer.querySelector('#btn-add-asset')?.addEventListener('click', () => { const dummyAsset = { id: '', category: config.title }; config.onRowClick && config.onRowClick(dummyAsset); }); actionContainer.querySelector('#btn-pc-flow')?.addEventListener('click', () => { window.dispatchEvent(new CustomEvent('open-pc-flow')); }); actionContainer.querySelector('#btn-goto-parts-master')?.addEventListener('click', () => { state.activeSubTab = '부품 마스터'; window.dispatchEvent(new Event('refresh-view')); }); } // 서버 탭 전용 목록보기 체크박스 이벤트 if (isServer) { const chkBox = filterBar.querySelector('#chk-list-view') as HTMLInputElement; const handleToggle = () => { const isListMode = chkBox.checked; if (isListMode) { state.viewMode = 'list'; } else { state.viewMode = 'location'; } window.dispatchEvent(new Event('refresh-view')); }; chkBox?.addEventListener('change', handleToggle); } switchView(); }