- 페이지 폴더 재구성: safety/, attendance/ 폴더 신규 생성 - work/ → safety/: 이슈 신고, 출입 신청 관련 페이지 이동 - common/ → attendance/: 근태/휴가 관련 페이지 이동 - admin/ 정리: safety-* 파일들을 safety/로 이동 - 사이드바 네비게이션 메뉴 구현 - 카테고리별 메뉴: 작업관리, 안전관리, 근태관리, 시스템관리 - 접기/펼치기 기능 및 상태 저장 - 관리자 전용 메뉴 자동 표시/숨김 - 날씨 API 연동 (기상청 단기예보) - TBM 및 navbar에 현재 날씨 표시 - weatherService.js 추가 - 안전 체크리스트 확장 - 기본/날씨별/작업별 체크 유형 추가 - checklist-manage.html 페이지 추가 - 이슈 신고 시스템 구현 - workIssueController, workIssueModel, workIssueRoutes 추가 - DB 마이그레이션 파일 추가 (실행 대기) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
222 lines
7.1 KiB
JavaScript
222 lines
7.1 KiB
JavaScript
/**
|
|
* 문제 신고 목록 페이지 JavaScript
|
|
*/
|
|
|
|
const API_BASE = window.API_BASE_URL || 'http://localhost:20005/api';
|
|
|
|
// 상태 한글 변환
|
|
const STATUS_LABELS = {
|
|
reported: '신고',
|
|
received: '접수',
|
|
in_progress: '처리중',
|
|
completed: '완료',
|
|
closed: '종료'
|
|
};
|
|
|
|
// 유형 한글 변환
|
|
const TYPE_LABELS = {
|
|
nonconformity: '부적합',
|
|
safety: '안전'
|
|
};
|
|
|
|
// DOM 요소
|
|
let issueList;
|
|
let filterStatus, filterType, filterStartDate, filterEndDate;
|
|
|
|
// 초기화
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
issueList = document.getElementById('issueList');
|
|
filterStatus = document.getElementById('filterStatus');
|
|
filterType = document.getElementById('filterType');
|
|
filterStartDate = document.getElementById('filterStartDate');
|
|
filterEndDate = document.getElementById('filterEndDate');
|
|
|
|
// 필터 이벤트 리스너
|
|
filterStatus.addEventListener('change', loadIssues);
|
|
filterType.addEventListener('change', loadIssues);
|
|
filterStartDate.addEventListener('change', loadIssues);
|
|
filterEndDate.addEventListener('change', loadIssues);
|
|
|
|
// 데이터 로드
|
|
await Promise.all([loadStats(), loadIssues()]);
|
|
});
|
|
|
|
/**
|
|
* 통계 로드
|
|
*/
|
|
async function loadStats() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/work-issues/stats/summary`, {
|
|
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// 권한이 없는 경우 (일반 사용자)
|
|
document.getElementById('statsGrid').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.success && data.data) {
|
|
document.getElementById('statReported').textContent = data.data.reported || 0;
|
|
document.getElementById('statReceived').textContent = data.data.received || 0;
|
|
document.getElementById('statProgress').textContent = data.data.in_progress || 0;
|
|
document.getElementById('statCompleted').textContent = data.data.completed || 0;
|
|
}
|
|
} catch (error) {
|
|
console.error('통계 로드 실패:', error);
|
|
document.getElementById('statsGrid').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 신고 목록 로드
|
|
*/
|
|
async function loadIssues() {
|
|
try {
|
|
// 필터 파라미터 구성
|
|
const params = new URLSearchParams();
|
|
|
|
if (filterStatus.value) params.append('status', filterStatus.value);
|
|
if (filterType.value) params.append('category_type', filterType.value);
|
|
if (filterStartDate.value) params.append('start_date', filterStartDate.value);
|
|
if (filterEndDate.value) params.append('end_date', filterEndDate.value);
|
|
|
|
const response = await fetch(`${API_BASE}/work-issues?${params.toString()}`, {
|
|
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
|
});
|
|
|
|
if (!response.ok) throw new Error('목록 조회 실패');
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
renderIssues(data.data || []);
|
|
}
|
|
} catch (error) {
|
|
console.error('신고 목록 로드 실패:', error);
|
|
issueList.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-title">목록을 불러올 수 없습니다</div>
|
|
<p>잠시 후 다시 시도해주세요.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 신고 목록 렌더링
|
|
*/
|
|
function renderIssues(issues) {
|
|
if (issues.length === 0) {
|
|
issueList.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-title">등록된 신고가 없습니다</div>
|
|
<p>새로운 문제를 신고하려면 '새 신고' 버튼을 클릭하세요.</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const baseUrl = (window.API_BASE_URL || 'http://localhost:20005').replace('/api', '');
|
|
|
|
issueList.innerHTML = issues.map(issue => {
|
|
const reportDate = new Date(issue.report_date).toLocaleString('ko-KR', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
|
|
// 위치 정보
|
|
let location = issue.custom_location || '';
|
|
if (issue.factory_name) {
|
|
location = issue.factory_name;
|
|
if (issue.workplace_name) {
|
|
location += ` - ${issue.workplace_name}`;
|
|
}
|
|
}
|
|
|
|
// 신고 제목 (항목명 또는 카테고리명)
|
|
const title = issue.issue_item_name || issue.issue_category_name || '신고';
|
|
|
|
// 사진 목록
|
|
const photos = [
|
|
issue.photo_path1,
|
|
issue.photo_path2,
|
|
issue.photo_path3,
|
|
issue.photo_path4,
|
|
issue.photo_path5
|
|
].filter(Boolean);
|
|
|
|
return `
|
|
<div class="issue-card" onclick="viewIssue(${issue.report_id})">
|
|
<div class="issue-header">
|
|
<span class="issue-id">#${issue.report_id}</span>
|
|
<span class="issue-status ${issue.status}">${STATUS_LABELS[issue.status] || issue.status}</span>
|
|
</div>
|
|
|
|
<div class="issue-title">
|
|
<span class="issue-type-badge ${issue.category_type}">${TYPE_LABELS[issue.category_type] || ''}</span>
|
|
${title}
|
|
</div>
|
|
|
|
<div class="issue-meta">
|
|
<span class="issue-meta-item">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
|
<circle cx="12" cy="7" r="4"/>
|
|
</svg>
|
|
${issue.reporter_full_name || issue.reporter_name}
|
|
</span>
|
|
<span class="issue-meta-item">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
|
<line x1="16" y1="2" x2="16" y2="6"/>
|
|
<line x1="8" y1="2" x2="8" y2="6"/>
|
|
<line x1="3" y1="10" x2="21" y2="10"/>
|
|
</svg>
|
|
${reportDate}
|
|
</span>
|
|
${location ? `
|
|
<span class="issue-meta-item">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
|
<circle cx="12" cy="10" r="3"/>
|
|
</svg>
|
|
${location}
|
|
</span>
|
|
` : ''}
|
|
${issue.assigned_full_name ? `
|
|
<span class="issue-meta-item">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
|
<circle cx="9" cy="7" r="4"/>
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
|
</svg>
|
|
담당: ${issue.assigned_full_name}
|
|
</span>
|
|
` : ''}
|
|
</div>
|
|
|
|
${photos.length > 0 ? `
|
|
<div class="issue-photos">
|
|
${photos.slice(0, 3).map(p => `
|
|
<img src="${baseUrl}${p}" alt="신고 사진" loading="lazy">
|
|
`).join('')}
|
|
${photos.length > 3 ? `<span style="display: flex; align-items: center; color: var(--gray-500);">+${photos.length - 3}</span>` : ''}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* 신고 상세 보기
|
|
*/
|
|
function viewIssue(reportId) {
|
|
window.location.href = `/pages/safety/issue-detail.html?id=${reportId}`;
|
|
}
|