refactor(frontend): 대시보드 섹션 로더 로직 개선
- admin.html의 잘못된 스크립트 태그 제거 - load-sections.js 리팩토링으로 코드 중복 제거 및 성능 최적화 - Promise.all과 DOMParser를 활용하여 화면 깜빡임 없이 동적 컨텐츠 로드
This commit is contained in:
@@ -1,187 +1,104 @@
|
||||
// ✅ /js/load-sections.js - 확장 가능한 구조 (개선됨)
|
||||
import { API, getAuthHeaders } from '/js/api-config.js';
|
||||
// /js/load-sections.js
|
||||
import { getUser } from './auth.js';
|
||||
import { apiGet } from './api-helper.js';
|
||||
|
||||
// 역할별 섹션 매핑 (쉽게 추가/수정 가능)
|
||||
// 역할에 따라 불러올 섹션 HTML 파일을 매핑합니다.
|
||||
const SECTION_MAP = {
|
||||
'admin': '/components/sections/admin-sections.html',
|
||||
'system': '/components/sections/admin-sections.html',
|
||||
'leader': '/components/sections/leader-sections.html',
|
||||
'group_leader': '/components/sections/leader-sections.html',
|
||||
'support': '/components/sections/support-sections.html',
|
||||
'support_team': '/components/sections/support-sections.html',
|
||||
'user': '/components/sections/user-sections.html',
|
||||
'worker': '/components/sections/user-sections.html'
|
||||
admin: '/components/sections/admin-sections.html',
|
||||
system: '/components/sections/admin-sections.html', // system도 admin과 동일한 섹션을 사용
|
||||
leader: '/components/sections/leader-sections.html',
|
||||
user: '/components/sections/user-sections.html',
|
||||
default: '/components/sections/user-sections.html', // 역할이 없는 경우 기본값
|
||||
};
|
||||
|
||||
// 공통 섹션 (모든 사용자에게 표시)
|
||||
const COMMON_SECTIONS = '/components/sections/common-sections.html';
|
||||
|
||||
async function loadSections() {
|
||||
/**
|
||||
* API를 통해 대시보드 통계 데이터를 가져옵니다.
|
||||
* @returns {Promise<object|null>} 통계 데이터 또는 에러 시 null
|
||||
*/
|
||||
async function fetchDashboardStats() {
|
||||
try {
|
||||
console.log('🔄 섹션 로딩 시작');
|
||||
|
||||
// 사용자 정보 확인
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
console.log('❌ 토큰 없음, 로그인 페이지로 이동');
|
||||
window.location.href = '/index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
let userInfo = { role: 'user', access_level: 'worker' };
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
userInfo = {
|
||||
role: payload.role || 'user',
|
||||
access_level: payload.access_level || 'worker'
|
||||
};
|
||||
console.log('👤 사용자 정보:', userInfo);
|
||||
} catch (err) {
|
||||
console.warn('⚠️ JWT 파싱 실패:', err);
|
||||
}
|
||||
|
||||
// ✅ 컨테이너 찾기 - 더 안전한 방식
|
||||
const possibleContainers = [
|
||||
'#sections-container',
|
||||
'#admin-sections',
|
||||
'#user-sections',
|
||||
'main[id$="-sections"]',
|
||||
'#content-container main'
|
||||
];
|
||||
|
||||
let container = null;
|
||||
for (const selector of possibleContainers) {
|
||||
container = document.querySelector(selector);
|
||||
if (container) {
|
||||
console.log(`✅ 컨테이너 발견: ${selector}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!container) {
|
||||
console.error('❌ 섹션 컨테이너를 찾을 수 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="loading">콘텐츠를 불러오는 중...</div>';
|
||||
|
||||
// 역할별 섹션 파일 결정 (수정된 버전)
|
||||
console.log('🔍 사용자 정보 디버깅:');
|
||||
console.log('- userInfo.role:', userInfo.role);
|
||||
console.log('- userInfo.access_level:', userInfo.access_level);
|
||||
|
||||
// role이 없으므로 access_level을 우선 사용
|
||||
const effectiveRole = userInfo.access_level || userInfo.role || 'user';
|
||||
const sectionFile = SECTION_MAP[effectiveRole] || SECTION_MAP['user'];
|
||||
|
||||
console.log(`📄 실제 사용될 역할: ${effectiveRole}`);
|
||||
console.log(`📄 로딩할 섹션 파일: ${sectionFile}`);
|
||||
|
||||
try {
|
||||
// 1. 공통 섹션 로드 (있을 경우)
|
||||
let commonHtml = '';
|
||||
try {
|
||||
console.log('📄 공통 섹션 로딩 시도');
|
||||
const commonRes = await fetch(COMMON_SECTIONS);
|
||||
if (commonRes.ok) {
|
||||
commonHtml = await commonRes.text();
|
||||
console.log('✅ 공통 섹션 로딩 성공');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('ℹ️ 공통 섹션 없음 (정상)');
|
||||
}
|
||||
|
||||
// 2. 역할별 섹션 로드
|
||||
console.log('📄 역할별 섹션 로딩 시도');
|
||||
const res = await fetch(sectionFile);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: 섹션 파일을 찾을 수 없습니다 (${sectionFile})`);
|
||||
}
|
||||
|
||||
const roleHtml = await res.text();
|
||||
console.log('✅ 역할별 섹션 로딩 성공');
|
||||
|
||||
// 3. 조합하여 표시
|
||||
container.innerHTML = commonHtml + roleHtml;
|
||||
console.log('✅ 섹션 HTML 렌더링 완료');
|
||||
|
||||
// 4. 추가 데이터 로드 (필요시)
|
||||
await loadDynamicData(userInfo);
|
||||
console.log('✅ 섹션 로딩 완료');
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ 섹션 로드 실패:', err);
|
||||
container.innerHTML = `
|
||||
<div class="error-state">
|
||||
<h3>❌ 콘텐츠를 불러올 수 없습니다</h3>
|
||||
<p>오류: ${err.message}</p>
|
||||
<p>잠시 후 다시 시도해주세요.</p>
|
||||
<button onclick="location.reload()" class="btn btn-primary">🔄 새로고침</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('🔴 섹션 로딩 실패:', err);
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
// 실제 백엔드 엔드포인트는 /api/dashboard/stats 와 같은 형태로 구현될 수 있습니다.
|
||||
const stats = await apiGet(`/workreports?start=${today}&end=${today}`);
|
||||
// 필요한 데이터 형태로 가공 (예시)
|
||||
return {
|
||||
today_reports_count: stats.length,
|
||||
today_workers_count: new Set(stats.map(d => d.worker_id)).size,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('대시보드 통계 데이터 로드 실패:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 동적 데이터 로드 (예: 대시보드 통계)
|
||||
async function loadDynamicData(userInfo) {
|
||||
console.log('📊 동적 데이터 로딩 시작');
|
||||
/**
|
||||
* 가상 DOM에 통계 데이터를 채워 넣습니다.
|
||||
* @param {Document} doc - 파싱된 HTML 문서 객체
|
||||
* @param {object} stats - 통계 데이터
|
||||
*/
|
||||
function populateStatsData(doc, stats) {
|
||||
if (!stats) return;
|
||||
|
||||
const todayStatsEl = doc.getElementById('today-stats');
|
||||
if (todayStatsEl) {
|
||||
todayStatsEl.innerHTML = `
|
||||
<p>📝 오늘 등록된 작업: ${stats.today_reports_count}건</p>
|
||||
<p>👥 참여 작업자: ${stats.today_workers_count}명</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메인 로직: 페이지에 역할별 섹션을 로드하고 내용을 채웁니다.
|
||||
*/
|
||||
async function initializeSections() {
|
||||
const mainContainer = document.querySelector('main[id$="-sections"]');
|
||||
if (!mainContainer) {
|
||||
console.error('섹션을 담을 메인 컨테이너를 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
mainContainer.innerHTML = '<div class="loading">콘텐츠를 불러오는 중...</div>';
|
||||
|
||||
const currentUser = getUser();
|
||||
if (!currentUser) {
|
||||
mainContainer.innerHTML = '<div class="error-state">사용자 정보를 찾을 수 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 오늘의 작업 현황
|
||||
const todayStats = document.getElementById('today-stats');
|
||||
if (todayStats) {
|
||||
try {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const res = await fetch(`${API}/workreports?start=${today}&end=${today}`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
todayStats.innerHTML = `
|
||||
<p>📝 오늘 등록된 작업: ${data.length}건</p>
|
||||
<p>👥 참여 작업자: ${new Set(data.map(d => d.worker_id)).size}명</p>
|
||||
`;
|
||||
console.log('✅ 오늘 통계 로딩 완료');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('❌ 통계 로드 실패:', e);
|
||||
if (todayStats) {
|
||||
todayStats.innerHTML = '<p>⚠️ 통계를 불러올 수 없습니다</p>';
|
||||
}
|
||||
const sectionFile = SECTION_MAP[currentUser.role] || SECTION_MAP.default;
|
||||
|
||||
try {
|
||||
// 1. 역할에 맞는 HTML 템플릿과 동적 데이터를 동시에 로드 (Promise.all 활용)
|
||||
const [htmlResponse, statsData] = await Promise.all([
|
||||
fetch(sectionFile),
|
||||
fetchDashboardStats()
|
||||
]);
|
||||
|
||||
if (!htmlResponse.ok) {
|
||||
throw new Error(`섹션 파일(${sectionFile})을 불러오는 데 실패했습니다.`);
|
||||
}
|
||||
}
|
||||
const htmlText = await htmlResponse.text();
|
||||
|
||||
// 빠른 링크 활성화
|
||||
initializeQuickLinks(userInfo);
|
||||
// 2. 텍스트를 가상 DOM으로 파싱
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlText, 'text/html');
|
||||
|
||||
// 3. (필요 시) 역할 기반으로 가상 DOM 필터링 - 현재는 파일 자체가 역할별로 나뉘어 불필요
|
||||
// filterByRole(doc, currentUser.role);
|
||||
|
||||
// 4. 가상 DOM에 동적 데이터 채우기
|
||||
populateStatsData(doc, statsData);
|
||||
|
||||
// 5. 모든 수정이 완료된 HTML을 실제 DOM에 한 번에 삽입
|
||||
mainContainer.innerHTML = doc.body.innerHTML;
|
||||
|
||||
console.log(`✅ ${currentUser.role} 역할의 섹션 로딩 완료.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('섹션 로딩 중 오류 발생:', error);
|
||||
mainContainer.innerHTML = `<div class="error-state">콘텐츠 로딩에 실패했습니다: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 권한별 빠른 링크 표시/숨김
|
||||
function initializeQuickLinks(userInfo) {
|
||||
console.log('🔗 빠른 링크 초기화');
|
||||
|
||||
// 권한에 따라 특정 링크 숨기기
|
||||
if (userInfo.role !== 'admin' && userInfo.access_level !== 'admin') {
|
||||
document.querySelectorAll('.admin-only').forEach(el => {
|
||||
el.style.display = 'none';
|
||||
console.log('🔒 관리자 전용 링크 숨김');
|
||||
});
|
||||
}
|
||||
|
||||
if (userInfo.access_level !== 'group_leader') {
|
||||
document.querySelectorAll('.leader-only').forEach(el => {
|
||||
el.style.display = 'none';
|
||||
console.log('🔒 그룹장 전용 링크 숨김');
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ 빠른 링크 초기화 완료');
|
||||
}
|
||||
|
||||
// 페이지 로드 시 실행
|
||||
document.addEventListener('DOMContentLoaded', loadSections);
|
||||
|
||||
// 수동 새로고침 함수 (다른 곳에서 호출 가능)
|
||||
window.refreshSections = loadSections;
|
||||
// DOM이 로드되면 섹션 초기화를 시작합니다.
|
||||
document.addEventListener('DOMContentLoaded', initializeSections);
|
||||
@@ -29,7 +29,6 @@
|
||||
<script type="module" src="/js/load-sidebar.js"></script>
|
||||
<script type="module" src="/js/load-sections.js"></script>
|
||||
<!-- ✅ admin.js는 다른 모듈들이 로딩된 후 실행되도록 순서 조정 -->
|
||||
<script type="module" src="/components/sections/admin-sections.html"></script>
|
||||
<script type="module" src="/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user