refactor(frontend): 대시보드 섹션 로더 로직 개선

- admin.html의 잘못된 스크립트 태그 제거
- load-sections.js 리팩토링으로 코드 중복 제거 및 성능 최적화
- Promise.all과 DOMParser를 활용하여 화면 깜빡임 없이 동적 컨텐츠 로드
This commit is contained in:
2025-07-28 12:05:27 +09:00
parent 8d7422d376
commit 892215a15d
2 changed files with 91 additions and 175 deletions

View File

@@ -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);

View File

@@ -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>