Files
tk-factory-services/system1-factory/web/public/js/my-dashboard.js
Hyungi Ahn ba9ef32808 security: 보안 강제 시스템 구축 + 하드코딩 비밀번호 제거
보안 감사 결과 CRITICAL 2건, HIGH 5건 발견 → 수정 완료 + 자동화 구축.

[보안 수정]
- issue-view.js: 하드코딩 비밀번호 → crypto.getRandomValues() 랜덤 생성
- pushSubscriptionController.js: ntfy 비밀번호 → process.env.NTFY_SUB_PASSWORD
- DEPLOY-GUIDE.md/PROGRESS.md/migration SQL: 평문 비밀번호 → placeholder
- docker-compose.yml/.env.example: NTFY_SUB_PASSWORD 환경변수 추가

[보안 강제 시스템 - 신규]
- scripts/security-scan.sh: 8개 규칙 (CRITICAL 2, HIGH 4, MEDIUM 2)
  3모드(staged/all/diff), severity, .securityignore, MEDIUM 임계값
- .githooks/pre-commit: 로컬 빠른 피드백
- .githooks/pre-receive-server.sh: Gitea 서버 최종 차단
  bypass 거버넌스([SECURITY-BYPASS: 사유] + 사용자 제한 + 로그)
- SECURITY-CHECKLIST.md: 10개 카테고리 자동/수동 구분
- docs/SECURITY-GUIDE.md: 운영자 가이드 (워크플로우, bypass, FAQ)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:44:21 +09:00

188 lines
5.4 KiB
JavaScript

// My Dashboard - 나의 대시보드 JavaScript
import './api-config.js';
// 전역 변수
let currentYear = new Date().getFullYear();
let currentMonth = new Date().getMonth() + 1;
// 페이지 초기화
document.addEventListener('DOMContentLoaded', async () => {
await loadUserInfo();
await loadVacationBalance();
await loadMonthlyCalendar();
await loadWorkHoursStats();
await loadRecentReports();
});
// 사용자 정보 로드
async function loadUserInfo() {
try {
const response = await apiCall('/users/me', 'GET');
const user = response.data || response;
document.getElementById('userName').textContent = user.name || '사용자';
document.getElementById('department').textContent = user.department || '-';
document.getElementById('jobType').textContent = user.job_type || '-';
document.getElementById('hireDate').textContent = user.hire_date || '-';
} catch (error) {
console.error('사용자 정보 로드 실패:', error);
}
}
// 연차 정보 로드
async function loadVacationBalance() {
try {
const response = await apiCall('/users/me/vacation-balance', 'GET');
const balance = response.data || response;
const total = balance.total_annual_leave || 15;
const used = balance.used_annual_leave || 0;
const remaining = total - used;
document.getElementById('totalLeave').textContent = total;
document.getElementById('usedLeave').textContent = used;
document.getElementById('remainingLeave').textContent = remaining;
// 프로그레스 바 업데이트
const percentage = (used / total) * 100;
document.getElementById('vacationProgress').style.width = `${percentage}%`;
} catch (error) {
console.error('연차 정보 로드 실패:', error);
}
}
// 월별 캘린더 로드
async function loadMonthlyCalendar() {
try {
const response = await apiCall(
`/users/me/attendance-records?year=${currentYear}&month=${currentMonth}`,
'GET'
);
const records = response.data || response;
renderCalendar(currentYear, currentMonth, records);
document.getElementById('currentMonth').textContent = `${currentYear}${currentMonth}`;
} catch (error) {
console.error('캘린더 로드 실패:', error);
renderCalendar(currentYear, currentMonth, []);
}
}
// 캘린더 렌더링
function renderCalendar(year, month, records) {
const calendar = document.getElementById('calendar');
const firstDay = new Date(year, month - 1, 1).getDay();
const daysInMonth = new Date(year, month, 0).getDate();
let html = '';
// 요일 헤더
const weekdays = ['일', '월', '화', '수', '목', '금', '토'];
weekdays.forEach(day => {
html += `<div class="calendar-header">${day}</div>`;
});
// 빈 칸
for (let i = 0; i < firstDay; i++) {
html += '<div class="calendar-day empty"></div>';
}
// 날짜
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const record = Array.isArray(records) ? records.find(r => r.record_date === dateStr) : null;
let statusClass = '';
if (record) {
const typeCode = record.attendance_type_code || record.type_code || '';
statusClass = typeCode.toLowerCase();
}
html += `
<div class="calendar-day ${statusClass}" title="${dateStr}">
<span class="day-number">${day}</span>
</div>
`;
}
calendar.innerHTML = html;
}
// 근무 시간 통계 로드
async function loadWorkHoursStats() {
try {
const response = await apiCall(
`/users/me/monthly-stats?year=${currentYear}&month=${currentMonth}`,
'GET'
);
const stats = response.data || response;
document.getElementById('monthHours').textContent = stats.month_hours || 0;
document.getElementById('workDays').textContent = stats.work_days || 0;
} catch (error) {
console.error('근무 시간 통계 로드 실패:', error);
}
}
// 최근 작업 보고서 로드
async function loadRecentReports() {
try {
const endDate = new Date().toISOString().split('T')[0];
const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString().split('T')[0];
const response = await apiCall(
`/users/me/work-reports?startDate=${startDate}&endDate=${endDate}`,
'GET'
);
const reports = response.data || response;
const list = document.getElementById('recentReportsList');
if (!Array.isArray(reports) || reports.length === 0) {
list.innerHTML = '<p class="empty-message">최근 7일간의 작업 보고서가 없습니다.</p>';
return;
}
list.innerHTML = reports.map(r => `
<div class="report-item">
<span class="date">${r.report_date}</span>
<span class="project">${r.project_name || 'N/A'}</span>
<span class="hours">${r.work_hours}시간</span>
</div>
`).join('');
} catch (error) {
console.error('최근 작업 보고서 로드 실패:', error);
}
}
// 이전 달
function previousMonth() {
currentMonth--;
if (currentMonth < 1) {
currentMonth = 12;
currentYear--;
}
loadMonthlyCalendar();
loadWorkHoursStats();
}
// 다음 달
function nextMonth() {
currentMonth++;
if (currentMonth > 12) {
currentMonth = 1;
currentYear++;
}
loadMonthlyCalendar();
loadWorkHoursStats();
}
// 전역 함수 노출
window.previousMonth = previousMonth;
window.nextMonth = nextMonth;
window.loadMonthlyCalendar = loadMonthlyCalendar;