- tkuser: 권한 관리를 별도 탭으로 분리, 부서 클릭 시 소속 인원 목록 표시 - system1: 모바일 UI 개선, nginx 권한 보정, 신고 카테고리 타입 마이그레이션 - system2: 신고 상세/보고서 개선, 내 보고서 페이지 추가 - system3: 이슈 뷰/수신함/관리함 개선 - gateway: 포털 라우팅 수정 - user-management API: 부서별 권한 벌크 설정 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
241 lines
7.9 KiB
JavaScript
241 lines
7.9 KiB
JavaScript
/**
|
|
* 내 신고 현황 페이지 JavaScript
|
|
* 전체 유형(안전/시설설비/부적합) 통합 목록
|
|
*/
|
|
|
|
const API_BASE = window.API_BASE_URL || 'http://localhost:30005/api';
|
|
|
|
// 상태 한글 변환
|
|
const STATUS_LABELS = {
|
|
reported: '신고',
|
|
received: '접수',
|
|
in_progress: '처리중',
|
|
completed: '완료',
|
|
closed: '종료'
|
|
};
|
|
|
|
// 유형 한글 변환
|
|
const TYPE_LABELS = {
|
|
safety: '안전',
|
|
facility: '시설설비',
|
|
nonconformity: '부적합'
|
|
};
|
|
|
|
// 유형별 배지 CSS 클래스
|
|
const TYPE_BADGE_CLASS = {
|
|
safety: 'type-badge-safety',
|
|
facility: 'type-badge-facility',
|
|
nonconformity: 'type-badge-nonconformity'
|
|
};
|
|
|
|
// DOM 요소
|
|
let issueList;
|
|
let filterType, filterStatus, filterStartDate, filterEndDate;
|
|
|
|
// 초기화
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
issueList = document.getElementById('issueList');
|
|
filterType = document.getElementById('filterType');
|
|
filterStatus = document.getElementById('filterStatus');
|
|
filterStartDate = document.getElementById('filterStartDate');
|
|
filterEndDate = document.getElementById('filterEndDate');
|
|
|
|
// 필터 이벤트 리스너
|
|
filterType.addEventListener('change', loadIssues);
|
|
filterStatus.addEventListener('change', loadIssues);
|
|
filterStartDate.addEventListener('change', loadIssues);
|
|
filterEndDate.addEventListener('change', loadIssues);
|
|
|
|
// 데이터 로드
|
|
await loadIssues();
|
|
});
|
|
|
|
/**
|
|
* 클라이언트 사이드 통계 계산
|
|
*/
|
|
function computeStats(issues) {
|
|
const stats = { reported: 0, received: 0, in_progress: 0, completed: 0 };
|
|
|
|
issues.forEach(issue => {
|
|
if (stats.hasOwnProperty(issue.status)) {
|
|
stats[issue.status]++;
|
|
}
|
|
});
|
|
|
|
document.getElementById('statReported').textContent = stats.reported;
|
|
document.getElementById('statReceived').textContent = stats.received;
|
|
document.getElementById('statProgress').textContent = stats.in_progress;
|
|
document.getElementById('statCompleted').textContent = stats.completed;
|
|
}
|
|
|
|
/**
|
|
* 신고 목록 로드 (전체 유형)
|
|
*/
|
|
async function loadIssues() {
|
|
try {
|
|
const params = new URLSearchParams();
|
|
|
|
// 유형 필터 (선택한 경우만)
|
|
if (filterType.value) {
|
|
params.append('category_type', filterType.value);
|
|
}
|
|
|
|
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) {
|
|
const issues = data.data || [];
|
|
computeStats(issues);
|
|
renderIssues(issues);
|
|
}
|
|
} 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 typeName = TYPE_LABELS[issue.category_type] || escapeHtml(issue.category_type || '');
|
|
const typeBadgeClass = TYPE_BADGE_CLASS[issue.category_type] || 'type-badge-safety';
|
|
|
|
// 사진 목록
|
|
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">
|
|
<span class="issue-category-badge ${typeBadgeClass}">${typeName}</span>
|
|
#${safeReportId}
|
|
</span>
|
|
<span class="issue-status ${safeStatus}">${STATUS_LABELS[issue.status] || escapeHtml(issue.status || '-')}</span>
|
|
</div>
|
|
|
|
<div class="issue-title">
|
|
${categoryName ? `<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=my-reports`;
|
|
}
|