feat: SSO 쿠키 인증 통합 + 서브도메인 라우팅 아키텍처
- Path-based 라우팅을 서브도메인 기반으로 전환 (tkfb/tkreport/tkqc.technicalkorea.net) - 3개 시스템 프론트엔드에 SSO 쿠키 인증 통합 (domain=.technicalkorea.net, localStorage 폴백) - Gateway: 포털+로그인+System1 프록시, 쿠키 SSO 설정 - System 1: 토큰키 통일, nginx.conf 생성, 신고페이지 리다이렉트 - System 2: api-base.js/app-init.js 생성, getSSOToken() 통합 - System 3: TokenManager 쿠키 지원, 중앙 로그인 리다이렉트 - docker-compose.yml에 cloudflared 서비스 추가 - DEPLOY-GUIDE.md 배포 가이드 작성 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
137
system2-report/web/js/api-base.js
Normal file
137
system2-report/web/js/api-base.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// /js/api-base.js
|
||||
// API 기본 설정 및 보안 유틸리티 - System 2 (신고 시스템)
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ==================== SSO 쿠키 유틸리티 ====================
|
||||
|
||||
function cookieGet(name) {
|
||||
var match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
function cookieRemove(name) {
|
||||
var cookie = name + '=; path=/; max-age=0';
|
||||
if (window.location.hostname.includes('technicalkorea.net')) {
|
||||
cookie += '; domain=.technicalkorea.net';
|
||||
}
|
||||
document.cookie = cookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSO 토큰 가져오기 (쿠키 우선, localStorage 폴백)
|
||||
*/
|
||||
window.getSSOToken = function() {
|
||||
return cookieGet('sso_token') || localStorage.getItem('sso_token');
|
||||
};
|
||||
|
||||
window.getSSOUser = function() {
|
||||
var raw = cookieGet('sso_user') || localStorage.getItem('sso_user');
|
||||
try { return raw ? JSON.parse(raw) : null; } catch(e) { return null; }
|
||||
};
|
||||
|
||||
/**
|
||||
* 중앙 로그인 URL 반환 (System 2 → tkfb 도메인의 로그인으로)
|
||||
*/
|
||||
window.getLoginUrl = function() {
|
||||
var hostname = window.location.hostname;
|
||||
if (hostname.includes('technicalkorea.net')) {
|
||||
return window.location.protocol + '//tkfb.technicalkorea.net/login?redirect=' + encodeURIComponent(window.location.href);
|
||||
}
|
||||
return window.location.protocol + '//' + hostname + ':30000/login?redirect=' + encodeURIComponent(window.location.href);
|
||||
};
|
||||
|
||||
window.clearSSOAuth = function() {
|
||||
cookieRemove('sso_token');
|
||||
cookieRemove('sso_user');
|
||||
cookieRemove('sso_refresh_token');
|
||||
localStorage.removeItem('sso_token');
|
||||
localStorage.removeItem('sso_user');
|
||||
localStorage.removeItem('sso_refresh_token');
|
||||
};
|
||||
|
||||
// ==================== 보안 유틸리티 (XSS 방지) ====================
|
||||
|
||||
window.escapeHtml = function(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
if (typeof str !== 'string') str = String(str);
|
||||
|
||||
var htmlEntities = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
|
||||
return str.replace(/[&<>"'`=\/]/g, function(char) {
|
||||
return htmlEntities[char];
|
||||
});
|
||||
};
|
||||
|
||||
window.escapeUrl = function(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
return encodeURIComponent(String(str));
|
||||
};
|
||||
|
||||
// ==================== API 설정 ====================
|
||||
|
||||
var API_PORT = 30105;
|
||||
var API_PATH = '/api';
|
||||
|
||||
function getApiBaseUrl() {
|
||||
var hostname = window.location.hostname;
|
||||
var protocol = window.location.protocol;
|
||||
|
||||
// 프로덕션 환경 - 같은 도메인의 /api 경로 (system2-web nginx가 프록시)
|
||||
if (hostname.includes('technicalkorea.net')) {
|
||||
return protocol + '//' + hostname + API_PATH;
|
||||
}
|
||||
|
||||
// 개발 환경
|
||||
return protocol + '//' + hostname + ':' + API_PORT + API_PATH;
|
||||
}
|
||||
|
||||
var apiUrl = getApiBaseUrl();
|
||||
window.API_BASE_URL = apiUrl;
|
||||
window.API = apiUrl;
|
||||
|
||||
// 인증 헤더 생성 - SSO 토큰 사용 (쿠키/localStorage)
|
||||
window.getAuthHeaders = function() {
|
||||
var token = window.getSSOToken();
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? 'Bearer ' + token : ''
|
||||
};
|
||||
};
|
||||
|
||||
// API 호출 헬퍼
|
||||
window.apiCall = async function(endpoint, method, data) {
|
||||
method = method || 'GET';
|
||||
var url = window.API_BASE_URL + endpoint;
|
||||
var config = {
|
||||
method: method,
|
||||
headers: window.getAuthHeaders()
|
||||
};
|
||||
|
||||
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE')) {
|
||||
config.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
var response = await fetch(url, config);
|
||||
|
||||
// 401 Unauthorized 처리
|
||||
if (response.status === 401) {
|
||||
window.clearSSOAuth();
|
||||
window.location.href = window.getLoginUrl();
|
||||
throw new Error('인증이 만료되었습니다.');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
console.log('[System2] API 설정 완료:', window.API_BASE_URL);
|
||||
})();
|
||||
54
system2-report/web/js/app-init.js
Normal file
54
system2-report/web/js/app-init.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// /js/app-init.js
|
||||
// System 2 (신고 시스템) 앱 초기화 - SSO 인증 체크
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ===== 인증 함수 (api-base.js의 전역 헬퍼 활용) =====
|
||||
function isLoggedIn() {
|
||||
var token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
|
||||
return token && token !== 'undefined' && token !== 'null';
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
return window.getSSOUser ? window.getSSOUser() : (function() {
|
||||
var u = localStorage.getItem('sso_user');
|
||||
return u ? JSON.parse(u) : null;
|
||||
})();
|
||||
}
|
||||
|
||||
function clearAuthData() {
|
||||
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
|
||||
localStorage.removeItem('sso_token');
|
||||
localStorage.removeItem('sso_user');
|
||||
}
|
||||
|
||||
// ===== 메인 초기화 =====
|
||||
async function init() {
|
||||
// 1. 인증 확인
|
||||
if (!isLoggedIn()) {
|
||||
clearAuthData();
|
||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
var currentUser = getUser();
|
||||
if (!currentUser || !currentUser.username) {
|
||||
clearAuthData();
|
||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[System2] 인증 확인:', currentUser.username);
|
||||
}
|
||||
|
||||
// DOMContentLoaded 시 실행
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// 전역 노출
|
||||
window.appInit = { getUser: getUser, clearAuthData: clearAuthData, isLoggedIn: isLoggedIn };
|
||||
})();
|
||||
@@ -56,7 +56,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
async function loadCurrentUser() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/users/me`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -74,7 +74,7 @@ async function loadCurrentUser() {
|
||||
async function loadReportDetail() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/work-issues/${reportId}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -372,7 +372,7 @@ function renderActionButtons(d) {
|
||||
async function loadStatusLogs() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/work-issues/${reportId}/status-logs`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) return;
|
||||
@@ -432,7 +432,7 @@ async function receiveReport() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/work-issues/${reportId}/receive`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
@@ -457,7 +457,7 @@ async function startProcessing() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/work-issues/${reportId}/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
@@ -482,7 +482,7 @@ async function closeReport() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/work-issues/${reportId}/close`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
@@ -507,7 +507,7 @@ async function deleteReport() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/work-issues/${reportId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
@@ -529,7 +529,7 @@ async function openAssignModal() {
|
||||
// 사용자 목록 로드
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/users`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -569,7 +569,7 @@ async function submitAssign() {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
assigned_department: department,
|
||||
@@ -614,7 +614,7 @@ async function submitComplete() {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
resolution_notes: notes
|
||||
|
||||
@@ -111,7 +111,7 @@ function setupEventListeners() {
|
||||
async function loadFactories() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/workplaces/categories/active/list`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('공장 목록 조회 실패');
|
||||
@@ -166,7 +166,7 @@ async function onFactoryChange() {
|
||||
async function loadMapImage() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/workplaces/categories`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) return;
|
||||
@@ -196,7 +196,7 @@ async function loadMapImage() {
|
||||
async function loadMapRegions() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/workplaces/categories/${selectedFactoryId}/map-regions`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) return;
|
||||
@@ -226,7 +226,7 @@ async function loadTodayData() {
|
||||
try {
|
||||
// TBM 세션 로드
|
||||
const tbmResponse = await fetch(`${API_BASE}/tbm/sessions/date/${today}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (tbmResponse.ok) {
|
||||
@@ -248,7 +248,7 @@ async function loadTodayData() {
|
||||
|
||||
// 출입 신청 로드
|
||||
const visitResponse = await fetch(`${API_BASE}/workplace-visits/requests`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (visitResponse.ok) {
|
||||
@@ -595,7 +595,7 @@ function onTypeSelect(type) {
|
||||
async function loadCategories(type) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/work-issues/categories/type/${type}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('카테고리 조회 실패');
|
||||
@@ -654,7 +654,7 @@ function onCategorySelect(category) {
|
||||
async function loadItems(categoryId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/work-issues/items/category/${categoryId}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('항목 조회 실패');
|
||||
@@ -877,7 +877,7 @@ async function submitReport() {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}`
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
222
system2-report/web/js/safety-report-list.js
Normal file
222
system2-report/web/js/safety-report-list.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 안전신고 현황 페이지 JavaScript
|
||||
* category_type=safety 고정 필터
|
||||
*/
|
||||
|
||||
const API_BASE = window.API_BASE_URL || 'http://localhost:20005/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: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'
|
||||
});
|
||||
|
||||
// 위치 정보 (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`;
|
||||
}
|
||||
@@ -102,7 +102,7 @@ function setupEventListeners() {
|
||||
async function loadFactories() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/workplaces/categories/active/list`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('공장 목록 조회 실패');
|
||||
@@ -157,7 +157,7 @@ async function onFactoryChange() {
|
||||
async function loadMapImage() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/workplaces/categories`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) return;
|
||||
@@ -187,7 +187,7 @@ async function loadMapImage() {
|
||||
async function loadMapRegions() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/workplaces/categories/${selectedFactoryId}/map-regions`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) return;
|
||||
@@ -210,7 +210,7 @@ async function loadTodayData() {
|
||||
try {
|
||||
// TBM 세션 로드
|
||||
const tbmResponse = await fetch(`${API_BASE}/tbm/sessions/date/${today}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (tbmResponse.ok) {
|
||||
@@ -220,7 +220,7 @@ async function loadTodayData() {
|
||||
|
||||
// 출입 신청 로드
|
||||
const visitResponse = await fetch(`${API_BASE}/workplace-visits/requests`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (visitResponse.ok) {
|
||||
@@ -493,7 +493,7 @@ function onTypeSelect(type) {
|
||||
async function loadCategories(type) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/work-issues/categories/type/${type}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('카테고리 조회 실패');
|
||||
@@ -552,7 +552,7 @@ function onCategorySelect(category) {
|
||||
async function loadItems(categoryId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/work-issues/items/category/${categoryId}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('항목 조회 실패');
|
||||
@@ -706,7 +706,7 @@ async function submitReport() {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}`
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
@@ -286,7 +286,7 @@
|
||||
<input type="date" id="filterStartDate" title="시작일">
|
||||
<input type="date" id="filterEndDate" title="종료일">
|
||||
|
||||
<a href="/pages/safety/report.html?type=safety" class="btn-new-report">
|
||||
<a href="/pages/safety/issue-report.html?type=safety" class="btn-new-report">
|
||||
+ 안전 신고
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user