- tkuser 서비스 신규 추가 (API + Web) - 사용자/권한/프로젝트/부서/작업자/작업장/설비/작업/휴가 통합 관리 - 작업장 탭: 공장→작업장 드릴다운 네비게이션 + 구역지도 클릭 연동 - 작업 탭: 공정(work_types)→작업(tasks) 계층 관리 - 휴가 탭: 유형 관리 + 연차 배정(근로기준법 자동계산) - 전 시스템 SSO 쿠키 인증으로 통합 (.technicalkorea.net 공유) - System 2: 작업 이슈 리포트 기능 강화 - System 3: tkuser API 연동, 페이지 권한 체계 적용 - docker-compose에 tkuser-api, tkuser-web 서비스 추가 - ARCHITECTURE.md, DEPLOYMENT.md 문서 작성 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
223 lines
7.6 KiB
JavaScript
223 lines
7.6 KiB
JavaScript
/**
|
|
* 안전신고 현황 페이지 JavaScript
|
|
* category_type=safety 고정 필터
|
|
*/
|
|
|
|
const API_BASE = window.API_BASE_URL || 'http://localhost:30005/api';
|
|
const CATEGORY_TYPE = 'safety';
|
|
|
|
// 상태 한글 변환
|
|
const STATUS_LABELS = {
|
|
reported: '신고',
|
|
received: '접수',
|
|
in_progress: '처리중',
|
|
completed: '완료',
|
|
closed: '종료'
|
|
};
|
|
|
|
// DOM 요소
|
|
let issueList;
|
|
let filterStatus, filterStartDate, filterEndDate;
|
|
|
|
// 초기화
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
issueList = document.getElementById('issueList');
|
|
filterStatus = document.getElementById('filterStatus');
|
|
filterStartDate = document.getElementById('filterStartDate');
|
|
filterEndDate = document.getElementById('filterEndDate');
|
|
|
|
// 필터 이벤트 리스너
|
|
filterStatus.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?category_type=${CATEGORY_TYPE}`, {
|
|
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_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 {
|
|
// 필터 파라미터 구성 (category_type 고정)
|
|
const params = new URLSearchParams();
|
|
params.append('category_type', CATEGORY_TYPE);
|
|
|
|
if (filterStatus.value) params.append('status', filterStatus.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 ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_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:30005').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'
|
|
});
|
|
|
|
// 위치 정보 (escaped)
|
|
let location = escapeHtml(issue.custom_location || '');
|
|
if (issue.factory_name) {
|
|
location = escapeHtml(issue.factory_name);
|
|
if (issue.workplace_name) {
|
|
location += ` - ${escapeHtml(issue.workplace_name)}`;
|
|
}
|
|
}
|
|
|
|
// 신고 제목 (항목명 또는 카테고리명)
|
|
const title = escapeHtml(issue.issue_item_name || issue.issue_category_name || '안전 신고');
|
|
const categoryName = escapeHtml(issue.issue_category_name || '안전');
|
|
|
|
// 사진 목록
|
|
const photos = [
|
|
issue.photo_path1,
|
|
issue.photo_path2,
|
|
issue.photo_path3,
|
|
issue.photo_path4,
|
|
issue.photo_path5
|
|
].filter(Boolean);
|
|
|
|
// 안전한 값들
|
|
const safeReportId = parseInt(issue.report_id) || 0;
|
|
const validStatuses = ['reported', 'received', 'in_progress', 'completed', 'closed'];
|
|
const safeStatus = validStatuses.includes(issue.status) ? issue.status : 'reported';
|
|
const reporterName = escapeHtml(issue.reporter_full_name || issue.reporter_name || '-');
|
|
const assignedName = issue.assigned_full_name ? escapeHtml(issue.assigned_full_name) : '';
|
|
|
|
return `
|
|
<div class="issue-card" onclick="viewIssue(${safeReportId})">
|
|
<div class="issue-header">
|
|
<span class="issue-id">#${safeReportId}</span>
|
|
<span class="issue-status ${safeStatus}">${STATUS_LABELS[issue.status] || escapeHtml(issue.status || '-')}</span>
|
|
</div>
|
|
|
|
<div class="issue-title">
|
|
<span class="issue-category-badge">${categoryName}</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>
|
|
${reporterName}
|
|
</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>
|
|
` : ''}
|
|
${assignedName ? `
|
|
<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>
|
|
담당: ${assignedName}
|
|
</span>
|
|
` : ''}
|
|
</div>
|
|
|
|
${photos.length > 0 ? `
|
|
<div class="issue-photos">
|
|
${photos.slice(0, 3).map(p => `
|
|
<img src="${baseUrl}${encodeURI(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}&from=safety`;
|
|
}
|