/** * Project Master Overseas Dashboard JS * 기능: 데이터 로드, 활성도 분석, 인증 모달 제어, 크롤링 동기화 및 중단 */ // --- 글로벌 상태 관리 --- let rawData = []; let projectActivityDetails = []; let isCrawling = false; const CONTINENT_ORDER = { "아시아": 1, "아프리카": 2, "아메리카": 3, "지사": 4 }; // --- 초기화 --- async function init() { console.log("Dashboard Initializing..."); const container = document.getElementById('projectAccordion'); if (!container) return; await loadAvailableDates(); await loadDataByDate(); } // --- 데이터 통신 및 로드 --- async function loadAvailableDates() { try { const response = await fetch('/available-dates'); const dates = await response.json(); if (dates?.length > 0) { const selectHtml = ` `; const baseDateStrong = document.getElementById('baseDate'); if (baseDateStrong) baseDateStrong.innerHTML = selectHtml; } } catch (e) { console.error("날짜 로드 실패:", e); } } async function loadDataByDate(selectedDate = "") { try { await loadActivityAnalysis(selectedDate); const url = selectedDate ? `/project-data?date=${selectedDate}` : `/project-data?t=${Date.now()}`; const response = await fetch(url); const data = await response.json(); if (data.error) throw new Error(data.error); rawData = data.projects || []; renderDashboard(rawData); } catch (e) { console.error("데이터 로드 실패:", e); alert("데이터를 가져오는 데 실패했습니다."); } } async function loadActivityAnalysis(date = "") { const dashboard = document.getElementById('activityDashboard'); if (!dashboard) return; try { const url = date ? `/project-activity?date=${date}` : `/project-activity`; const response = await fetch(url); const data = await response.json(); if (data.error) return; const { summary, details } = data; projectActivityDetails = details; dashboard.innerHTML = `
정상 (7일 이내)
${summary.active}
주의 (14일 이내)
${summary.warning}
방치 (14일 초과 / 폴더자동삭제)
${summary.stale}
데이터 없음 (파일 0개)
${summary.unknown}
`; } catch (e) { console.error("분석 로드 실패:", e); } } // --- 렌더링 엔진 --- function renderDashboard(data) { const container = document.getElementById('projectAccordion'); container.innerHTML = ''; const grouped = groupData(data); Object.keys(grouped).sort((a, b) => (CONTINENT_ORDER[a] || 99) - (CONTINENT_ORDER[b] || 99)).forEach(continent => { const continentDiv = document.createElement('div'); continentDiv.className = 'continent-group active'; let html = `
${continent}
`; Object.keys(grouped[continent]).sort().forEach(country => { html += `
${country}
프로젝트명
담당부서
담당자
파일수
최근로그
${grouped[continent][country].sort((a, b) => a[0].localeCompare(b[0])).map(p => createProjectHtml(p)).join('')}
`; }); html += `
`; continentDiv.innerHTML = html; container.appendChild(continentDiv); }); } function groupData(data) { const res = {}; data.forEach(item => { const c1 = item[5] || "기타", c2 = item[6] || "미분류"; if (!res[c1]) res[c1] = {}; if (!res[c1][c2]) res[c1][c2] = []; res[c1][c2].push(item); }); return res; } function createProjectHtml(p) { const [name, dept, admin, logRaw, files] = p; const recentLog = (!logRaw || logRaw === "X" || logRaw === "데이터 없음") ? "기록 없음" : logRaw; const logTime = recentLog !== "기록 없음" ? recentLog.split(',')[0] : "기록 없음"; // '폴더 자동 삭제' 여부 확인 const isStaleLog = recentLog.replace(/\s/g, "").includes("폴더자동삭제"); // 파일이 0개 또는 NULL인 경우에만 행 전체 에러(붉은색) 표시 const isNoFiles = (files === 0 || files === null); const statusClass = isNoFiles ? "status-error" : ""; // 로그 텍스트 스타일 결정 // 폴더자동삭제는 위험(error), 기록 없음은 주의(warning) let logStyleClass = ""; if (isStaleLog) { logStyleClass = "error-text"; } else if (recentLog === "기록 없음") { logStyleClass = "warning-text"; } const logBoldStyle = isStaleLog ? 'font-weight: 800;' : ''; return `
${name}
${dept}
${admin}
${files || 0}
${recentLog}

참여 인원 상세

이름소속권한
${admin}${dept}관리자

최근 활동

유형내용일시
로그동기화 완료${logTime}
`; } // --- 이벤트 핸들러 --- function toggleGroup(h) { h.parentElement.classList.toggle('active'); } function toggleAccordion(h) { const item = h.parentElement; item.parentElement.querySelectorAll('.accordion-item').forEach(el => { if (el !== item) el.classList.remove('active'); }); item.classList.toggle('active'); } function showActivityDetails(status) { const modal = document.getElementById('activityDetailModal'), tbody = document.getElementById('modalTableBody'), title = document.getElementById('modalTitle'); const names = { active: '정상', warning: '주의', stale: '방치', unknown: '데이터 없음' }; const filtered = (projectActivityDetails || []).filter(d => d.status === status); title.innerText = `${names[status]} 목록 (${filtered.length}개)`; tbody.innerHTML = filtered.map(p => { const o = rawData.find(r => r[0] === p.name); return `${p.name}${o ? o[1] : "-"}${o ? o[2] : "-"}`; }).join(''); modal.style.display = 'flex'; } function closeActivityModal() { const modal = document.getElementById('activityDetailModal'); if (modal) modal.style.display = 'none'; } function closeAuthModal() { const modal = document.getElementById('authModal'); if (modal) modal.style.display = 'none'; } function scrollToProject(name) { closeActivityModal(); const target = Array.from(document.querySelectorAll('.repo-title')).find(t => t.innerText.trim() === name.trim())?.closest('.accordion-header'); if (target) { let p = target.parentElement; while (p && p !== document.body) { if (p.classList.contains('continent-group') || p.classList.contains('country-group')) p.classList.add('active'); p = p.parentElement; } target.parentElement.classList.add('active'); const pos = target.getBoundingClientRect().top + window.pageYOffset - 220; window.scrollTo({ top: pos, behavior: 'smooth' }); target.style.backgroundColor = 'var(--primary-lv-1)'; setTimeout(() => target.style.backgroundColor = '', 2000); } } // --- 크롤링 및 인증 제어 --- async function syncData() { if (isCrawling) { if (confirm("크롤링을 중단하시겠습니까?")) { const res = await fetch('/stop-sync'); if ((await res.json()).success) document.getElementById('syncBtn').innerText = "중단 요청 중..."; } return; } const modal = document.getElementById('authModal'); if (modal) { document.getElementById('authId').value = ''; document.getElementById('authPw').value = ''; document.getElementById('authErrorMessage').style.display = 'none'; modal.style.display = 'flex'; document.getElementById('authId').focus(); } } function closeAuthModal() { document.getElementById('authModal').style.display = 'none'; } async function submitAuth() { const id = document.getElementById('authId').value, pw = document.getElementById('authPw').value, err = document.getElementById('authErrorMessage'); try { const res = await fetch('/auth/crawl', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: id, password: pw }) }); const data = await res.json(); if (data.success) { closeAuthModal(); startCrawlProcess(); } else { err.innerText = "크롤링을 할 수 없습니다."; err.style.display = 'block'; } } catch { err.innerText = "서버 연결 실패"; err.style.display = 'block'; } } async function startCrawlProcess() { isCrawling = true; const btn = document.getElementById('syncBtn'), logC = document.getElementById('logConsole'), logB = document.getElementById('logBody'); btn.classList.add('loading'); btn.style.backgroundColor = 'var(--error-color)'; btn.innerHTML = ` 크롤링 중단`; logC.style.display = 'block'; logB.innerHTML = '
>>> 엔진 초기화 중...
'; try { const res = await fetch(`/sync`); const reader = res.body.getReader(), decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; decoder.decode(value).split('\n').forEach(line => { if (line.startsWith('data: ')) { const p = JSON.parse(line.substring(6)); if (p.type === 'log') { const div = document.createElement('div'); div.innerText = `[${new Date().toLocaleTimeString()}] ${p.message}`; logB.appendChild(div); logC.scrollTop = logC.scrollHeight; } else if (p.type === 'done') { init(); alert(`동기화 종료`); logC.style.display = 'none'; } } }); } } catch { alert("스트림 끊김"); } finally { isCrawling = false; btn.classList.remove('loading'); btn.style.backgroundColor = ''; btn.innerHTML = ` 데이터 동기화 (크롤링)`; } } document.addEventListener('DOMContentLoaded', init);