fix: 보안 취약점 수정 및 XSS 방지 적용
## 백엔드 보안 수정 - 하드코딩된 비밀번호 및 JWT 시크릿 폴백 제거 - SQL Injection 방지를 위한 화이트리스트 검증 추가 - 인증 미적용 API 라우트에 requireAuth 미들웨어 적용 - CSRF 보호 미들웨어 구현 (csrf.js) - 파일 업로드 보안 유틸리티 추가 (fileUploadSecurity.js) - 비밀번호 정책 검증 유틸리티 추가 (passwordValidator.js) ## 프론트엔드 XSS 방지 - api-base.js에 전역 escapeHtml() 함수 추가 - 17개 주요 JS 파일에 escapeHtml 적용: - tbm.js, daily-patrol.js, daily-work-report.js - task-management.js, workplace-status.js - equipment-detail.js, equipment-management.js - issue-detail.js, issue-report.js - vacation-common.js, worker-management.js - safety-report-list.js, nonconformity-list.js - project-management.js, workplace-management.js ## 정리 - 백업 폴더 및 빈 파일 삭제 - SECURITY_GUIDE.md 문서 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,48 @@
|
||||
// /js/api-base.js
|
||||
// API 기본 설정 (비모듈 - 빠른 로딩용)
|
||||
// API 기본 설정 및 보안 유틸리티 (비모듈 - 빠른 로딩용)
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ==================== 보안 유틸리티 (XSS 방지) ====================
|
||||
|
||||
/**
|
||||
* HTML 특수문자 이스케이프 (XSS 방지)
|
||||
* innerHTML에 사용자 입력/API 데이터를 삽입할 때 반드시 사용
|
||||
*
|
||||
* @param {string} str - 이스케이프할 문자열
|
||||
* @returns {string} 이스케이프된 문자열
|
||||
*/
|
||||
window.escapeHtml = function(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
if (typeof str !== 'string') str = String(str);
|
||||
|
||||
const htmlEntities = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
|
||||
return str.replace(/[&<>"'`=\/]/g, function(char) {
|
||||
return htmlEntities[char];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* URL 파라미터 이스케이프
|
||||
*/
|
||||
window.escapeUrl = function(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
return encodeURIComponent(String(str));
|
||||
};
|
||||
|
||||
// ==================== API 설정 ====================
|
||||
|
||||
const API_PORT = 20005;
|
||||
const API_PATH = '/api';
|
||||
|
||||
|
||||
@@ -44,20 +44,18 @@ function waitForAxiosConfig() {
|
||||
});
|
||||
}
|
||||
|
||||
// 자동 날짜/시간대 결정
|
||||
function getAutoPatrolDateTime() {
|
||||
const now = new Date();
|
||||
const patrolDate = now.toISOString().slice(0, 10);
|
||||
const hour = now.getHours();
|
||||
// 오전(~12시), 오후(12시~)
|
||||
const patrolTime = hour < 12 ? 'morning' : 'afternoon';
|
||||
return { patrolDate, patrolTime };
|
||||
}
|
||||
|
||||
// 페이지 초기화
|
||||
async function initializePage() {
|
||||
// 오늘 날짜 설정
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
document.getElementById('patrolDate').value = today;
|
||||
|
||||
// 시간대 버튼 이벤트
|
||||
document.querySelectorAll('.patrol-time-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.patrol-time-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// 데이터 로드
|
||||
await Promise.all([
|
||||
loadCategories(),
|
||||
@@ -72,15 +70,74 @@ async function loadCategories() {
|
||||
const response = await axios.get('/workplaces/categories');
|
||||
if (response.data.success) {
|
||||
categories = response.data.data;
|
||||
const select = document.getElementById('categorySelect');
|
||||
select.innerHTML = '<option value="">공장 선택...</option>' +
|
||||
categories.map(c => `<option value="${c.category_id}">${c.category_name}</option>`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('공장 목록 로드 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 공장 선택 화면 표시
|
||||
function showFactorySelection() {
|
||||
const { patrolDate, patrolTime } = getAutoPatrolDateTime();
|
||||
const timeLabel = patrolTime === 'morning' ? '오전' : '오후';
|
||||
const dateObj = new Date(patrolDate);
|
||||
const dateLabel = dateObj.toLocaleDateString('ko-KR', { month: 'long', day: 'numeric', weekday: 'short' });
|
||||
|
||||
// 세션 정보 표시
|
||||
document.getElementById('patrolSessionInfo').textContent = `${dateLabel} ${timeLabel} 순회점검`;
|
||||
|
||||
// 공장 카드 렌더링
|
||||
const container = document.getElementById('factoryCardsContainer');
|
||||
container.innerHTML = categories.map(c => `
|
||||
<div class="factory-card" onclick="selectFactory(${parseInt(c.category_id) || 0})">
|
||||
<div class="factory-card-icon">
|
||||
${c.layout_image ? `<img src="${escapeHtml(getImageUrl(c.layout_image))}" alt="${escapeHtml(c.category_name)}">` : '<span>🏭</span>'}
|
||||
</div>
|
||||
<div class="factory-card-name">${escapeHtml(c.category_name)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// 시작 버튼 숨기고 공장 선택 영역 표시
|
||||
document.getElementById('startPatrolBtn').style.display = 'none';
|
||||
document.getElementById('factorySelectionArea').style.display = 'block';
|
||||
}
|
||||
|
||||
// 공장 선택 후 점검 시작
|
||||
async function selectFactory(categoryId) {
|
||||
const { patrolDate, patrolTime } = getAutoPatrolDateTime();
|
||||
|
||||
try {
|
||||
// 세션 생성 또는 조회
|
||||
const response = await axios.post('/patrol/sessions', {
|
||||
patrol_date: patrolDate,
|
||||
patrol_time: patrolTime,
|
||||
category_id: categoryId
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
currentSession = response.data.data;
|
||||
currentSession.patrol_date = patrolDate;
|
||||
currentSession.patrol_time = patrolTime;
|
||||
currentSession.category_id = categoryId;
|
||||
|
||||
// 작업장 목록 로드
|
||||
await loadWorkplaces(categoryId);
|
||||
|
||||
// 체크리스트 항목 로드
|
||||
await loadChecklistItems(categoryId);
|
||||
|
||||
// 공장 선택 영역 숨기고 점검 영역 표시
|
||||
document.getElementById('factorySelectionArea').style.display = 'none';
|
||||
document.getElementById('patrolArea').style.display = 'block';
|
||||
renderSessionInfo();
|
||||
renderWorkplaceMap();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('순회점검 시작 실패:', error);
|
||||
alert('순회점검을 시작할 수 없습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 물품 유형 로드
|
||||
async function loadItemTypes() {
|
||||
try {
|
||||
@@ -133,63 +190,18 @@ function renderTodayStatus(statusList) {
|
||||
<div class="status-value ${morning?.status === 'completed' ? 'completed' : 'pending'}">
|
||||
${morning ? (morning.status === 'completed' ? '완료' : '진행중') : '미점검'}
|
||||
</div>
|
||||
${morning ? `<div class="status-sub">${morning.inspector_name || ''}</div>` : ''}
|
||||
${morning ? `<div class="status-sub">${escapeHtml(morning.inspector_name || '')}</div>` : ''}
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-label">오후</div>
|
||||
<div class="status-value ${afternoon?.status === 'completed' ? 'completed' : 'pending'}">
|
||||
${afternoon ? (afternoon.status === 'completed' ? '완료' : '진행중') : '미점검'}
|
||||
</div>
|
||||
${afternoon ? `<div class="status-sub">${afternoon.inspector_name || ''}</div>` : ''}
|
||||
${afternoon ? `<div class="status-sub">${escapeHtml(afternoon.inspector_name || '')}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 순회점검 시작
|
||||
async function startPatrol() {
|
||||
const patrolDate = document.getElementById('patrolDate').value;
|
||||
const patrolTime = document.querySelector('.patrol-time-btn.active')?.dataset.time;
|
||||
const categoryId = document.getElementById('categorySelect').value;
|
||||
|
||||
if (!patrolDate || !patrolTime || !categoryId) {
|
||||
alert('점검 일자, 시간대, 공장을 모두 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 세션 생성 또는 조회
|
||||
const response = await axios.post('/patrol/sessions', {
|
||||
patrol_date: patrolDate,
|
||||
patrol_time: patrolTime,
|
||||
category_id: categoryId
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
currentSession = response.data.data;
|
||||
currentSession.patrol_date = patrolDate;
|
||||
currentSession.patrol_time = patrolTime;
|
||||
currentSession.category_id = categoryId;
|
||||
|
||||
// 작업장 목록 로드
|
||||
await loadWorkplaces(categoryId);
|
||||
|
||||
// 체크리스트 항목 로드
|
||||
await loadChecklistItems(categoryId);
|
||||
|
||||
// 점검 영역 표시
|
||||
document.getElementById('patrolArea').style.display = 'block';
|
||||
renderSessionInfo();
|
||||
renderWorkplaceMap();
|
||||
|
||||
// 시작 버튼 비활성화
|
||||
document.getElementById('startPatrolBtn').textContent = '점검 진행중...';
|
||||
document.getElementById('startPatrolBtn').disabled = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('순회점검 시작 실패:', error);
|
||||
alert('순회점검을 시작할 수 없습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 작업장 목록 로드
|
||||
async function loadWorkplaces(categoryId) {
|
||||
@@ -227,7 +239,7 @@ function renderSessionInfo() {
|
||||
<div class="session-info">
|
||||
<div class="session-info-item">
|
||||
<span class="session-info-label">점검일자</span>
|
||||
<span class="session-info-value">${formatDate(currentSession.patrol_date)}</span>
|
||||
<span class="session-info-value">${escapeHtml(formatDate(currentSession.patrol_date))}</span>
|
||||
</div>
|
||||
<div class="session-info-item">
|
||||
<span class="session-info-label">시간대</span>
|
||||
@@ -235,14 +247,14 @@ function renderSessionInfo() {
|
||||
</div>
|
||||
<div class="session-info-item">
|
||||
<span class="session-info-label">공장</span>
|
||||
<span class="session-info-value">${category?.category_name || ''}</span>
|
||||
<span class="session-info-value">${escapeHtml(category?.category_name || '')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${progress}%"></div>
|
||||
<div class="progress-fill" style="width: ${parseInt(progress) || 0}%"></div>
|
||||
</div>
|
||||
<span class="progress-text">${progress}%</span>
|
||||
<span class="progress-text">${parseInt(progress) || 0}%</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -255,18 +267,19 @@ function renderWorkplaceMap() {
|
||||
|
||||
// 지도 이미지가 있으면 지도 표시
|
||||
if (category?.layout_image) {
|
||||
mapContainer.innerHTML = `<img src="${getImageUrl(category.layout_image)}" alt="${category.category_name} 지도">`;
|
||||
mapContainer.innerHTML = `<img src="${escapeHtml(getImageUrl(category.layout_image))}" alt="${escapeHtml(category.category_name)} 지도">`;
|
||||
mapContainer.style.display = 'block';
|
||||
listContainer.style.display = 'none';
|
||||
|
||||
// 작업장 마커 추가
|
||||
// 좌표가 있는 작업장만 마커 추가
|
||||
const hasMarkers = workplaces.some(wp => wp.x_percent && wp.y_percent);
|
||||
|
||||
workplaces.forEach(wp => {
|
||||
if (wp.x_percent && wp.y_percent) {
|
||||
const marker = document.createElement('div');
|
||||
marker.className = 'workplace-marker';
|
||||
marker.style.left = `${wp.x_percent}%`;
|
||||
marker.style.top = `${wp.y_percent}%`;
|
||||
marker.textContent = wp.workplace_name;
|
||||
marker.style.left = `${parseFloat(wp.x_percent) || 0}%`;
|
||||
marker.style.top = `${parseFloat(wp.y_percent) || 0}%`;
|
||||
marker.textContent = wp.workplace_name; // textContent는 자동 이스케이프
|
||||
marker.dataset.workplaceId = wp.workplace_id;
|
||||
marker.onclick = () => selectWorkplace(wp.workplace_id);
|
||||
|
||||
@@ -279,30 +292,43 @@ function renderWorkplaceMap() {
|
||||
mapContainer.appendChild(marker);
|
||||
}
|
||||
});
|
||||
|
||||
// 좌표가 없는 작업장이 있으면 카드 목록도 표시
|
||||
if (!hasMarkers || workplaces.some(wp => !wp.x_percent || !wp.y_percent)) {
|
||||
listContainer.style.display = 'grid';
|
||||
renderWorkplaceCards(listContainer);
|
||||
} else {
|
||||
listContainer.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// 지도 없으면 카드 목록으로 표시
|
||||
mapContainer.style.display = 'none';
|
||||
listContainer.style.display = 'grid';
|
||||
|
||||
listContainer.innerHTML = workplaces.map(wp => {
|
||||
const records = checkRecords[wp.workplace_id];
|
||||
const isCompleted = records && records.length > 0 && records.every(r => r.is_checked);
|
||||
const isInProgress = records && records.some(r => r.is_checked);
|
||||
|
||||
return `
|
||||
<div class="workplace-card ${isCompleted ? 'completed' : ''} ${selectedWorkplace?.workplace_id === wp.workplace_id ? 'selected' : ''}"
|
||||
data-workplace-id="${wp.workplace_id}"
|
||||
onclick="selectWorkplace(${wp.workplace_id})">
|
||||
<div class="workplace-card-name">${wp.workplace_name}</div>
|
||||
<div class="workplace-card-status">
|
||||
${isCompleted ? '점검완료' : (isInProgress ? '점검중' : '미점검')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
renderWorkplaceCards(listContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// 작업장 카드 렌더링
|
||||
function renderWorkplaceCards(container) {
|
||||
container.innerHTML = workplaces.map(wp => {
|
||||
const records = checkRecords[wp.workplace_id];
|
||||
const isCompleted = records && records.length > 0 && records.every(r => r.is_checked);
|
||||
const isInProgress = records && records.some(r => r.is_checked);
|
||||
const workplaceId = parseInt(wp.workplace_id) || 0;
|
||||
|
||||
return `
|
||||
<div class="workplace-card ${isCompleted ? 'completed' : ''} ${isInProgress && !isCompleted ? 'in-progress' : ''} ${selectedWorkplace?.workplace_id === wp.workplace_id ? 'selected' : ''}"
|
||||
data-workplace-id="${workplaceId}"
|
||||
onclick="selectWorkplace(${workplaceId})">
|
||||
<div class="workplace-card-name">${escapeHtml(wp.workplace_name)}</div>
|
||||
<div class="workplace-card-status">
|
||||
${isCompleted ? '점검완료' : (isInProgress ? '점검중' : '미점검')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 작업장 선택
|
||||
async function selectWorkplace(workplaceId) {
|
||||
selectedWorkplace = workplaces.find(w => w.workplace_id === workplaceId);
|
||||
@@ -345,7 +371,7 @@ function renderChecklist(workplaceId) {
|
||||
const workplace = workplaces.find(w => w.workplace_id === workplaceId);
|
||||
|
||||
header.innerHTML = `
|
||||
<h3>${workplace?.workplace_name || ''} 체크리스트</h3>
|
||||
<h3>${escapeHtml(workplace?.workplace_name || '')} 체크리스트</h3>
|
||||
<p class="checklist-subtitle">각 항목을 점검하고 체크해주세요</p>
|
||||
`;
|
||||
|
||||
@@ -362,32 +388,34 @@ function renderChecklist(workplaceId) {
|
||||
|
||||
content.innerHTML = Object.entries(grouped).map(([category, items]) => `
|
||||
<div class="checklist-category">
|
||||
<div class="checklist-category-title">${getCategoryName(category)}</div>
|
||||
<div class="checklist-category-title">${escapeHtml(getCategoryName(category))}</div>
|
||||
${items.map(item => {
|
||||
const record = records.find(r => r.check_item_id === item.item_id);
|
||||
const isChecked = record?.is_checked;
|
||||
const checkResult = record?.check_result;
|
||||
const itemId = parseInt(item.item_id) || 0;
|
||||
const wpId = parseInt(workplaceId) || 0;
|
||||
|
||||
return `
|
||||
<div class="check-item ${isChecked ? 'checked' : ''}"
|
||||
data-item-id="${item.item_id}"
|
||||
onclick="toggleCheckItem(${workplaceId}, ${item.item_id})">
|
||||
data-item-id="${itemId}"
|
||||
onclick="toggleCheckItem(${wpId}, ${itemId})">
|
||||
<div class="check-item-checkbox">
|
||||
${isChecked ? '✓' : ''}
|
||||
</div>
|
||||
<div class="check-item-content">
|
||||
<div class="check-item-text">
|
||||
${item.check_item}
|
||||
${escapeHtml(item.check_item)}
|
||||
${item.is_required ? '<span class="check-item-required">*</span>' : ''}
|
||||
</div>
|
||||
${isChecked ? `
|
||||
<div class="check-result-selector" onclick="event.stopPropagation()">
|
||||
<button class="check-result-btn good ${checkResult === 'good' ? 'active' : ''}"
|
||||
onclick="setCheckResult(${workplaceId}, ${item.item_id}, 'good')">양호</button>
|
||||
onclick="setCheckResult(${wpId}, ${itemId}, 'good')">양호</button>
|
||||
<button class="check-result-btn warning ${checkResult === 'warning' ? 'active' : ''}"
|
||||
onclick="setCheckResult(${workplaceId}, ${item.item_id}, 'warning')">주의</button>
|
||||
onclick="setCheckResult(${wpId}, ${itemId}, 'warning')">주의</button>
|
||||
<button class="check-result-btn bad ${checkResult === 'bad' ? 'active' : ''}"
|
||||
onclick="setCheckResult(${workplaceId}, ${item.item_id}, 'bad')">불량</button>
|
||||
onclick="setCheckResult(${wpId}, ${itemId}, 'bad')">불량</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -551,19 +579,21 @@ function renderItemsSection(workplaceId) {
|
||||
|
||||
// 작업장 레이아웃 이미지가 있으면 표시
|
||||
if (workplace?.layout_image) {
|
||||
container.innerHTML = `<img src="${getImageUrl(workplace.layout_image)}" alt="${workplace.workplace_name}">`;
|
||||
container.innerHTML = `<img src="${escapeHtml(getImageUrl(workplace.layout_image))}" alt="${escapeHtml(workplace.workplace_name)}">`;
|
||||
|
||||
// 물품 마커 추가
|
||||
workplaceItems.forEach(item => {
|
||||
if (item.x_percent && item.y_percent) {
|
||||
const marker = document.createElement('div');
|
||||
marker.className = `item-marker ${item.item_type}`;
|
||||
marker.style.left = `${item.x_percent}%`;
|
||||
marker.style.top = `${item.y_percent}%`;
|
||||
marker.style.width = `${item.width_percent || 5}%`;
|
||||
marker.style.height = `${item.height_percent || 5}%`;
|
||||
marker.innerHTML = item.icon || getItemTypeIcon(item.item_type);
|
||||
marker.title = `${item.item_name || item.type_name} (${item.quantity}개)`;
|
||||
// item_type은 화이트리스트로 검증
|
||||
const safeItemType = ['container', 'plate', 'material', 'tool', 'other'].includes(item.item_type) ? item.item_type : 'other';
|
||||
marker.className = `item-marker ${safeItemType}`;
|
||||
marker.style.left = `${parseFloat(item.x_percent) || 0}%`;
|
||||
marker.style.top = `${parseFloat(item.y_percent) || 0}%`;
|
||||
marker.style.width = `${parseFloat(item.width_percent) || 5}%`;
|
||||
marker.style.height = `${parseFloat(item.height_percent) || 5}%`;
|
||||
marker.textContent = item.icon || getItemTypeIcon(item.item_type); // textContent 사용
|
||||
marker.title = `${item.item_name || item.type_name} (${parseInt(item.quantity) || 0}개)`;
|
||||
marker.dataset.itemId = item.item_id;
|
||||
marker.onclick = () => openItemModal(item);
|
||||
container.appendChild(marker);
|
||||
@@ -593,7 +623,7 @@ function renderItemTypesSelect() {
|
||||
const select = document.getElementById('itemType');
|
||||
if (!select) return;
|
||||
select.innerHTML = itemTypes.map(t =>
|
||||
`<option value="${t.type_code}">${t.icon} ${t.type_name}</option>`
|
||||
`<option value="${escapeHtml(t.type_code)}">${escapeHtml(t.icon)} ${escapeHtml(t.type_name)}</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
@@ -601,14 +631,19 @@ function renderItemTypesSelect() {
|
||||
function renderItemsLegend() {
|
||||
const container = document.getElementById('itemsLegend');
|
||||
if (!container) return;
|
||||
container.innerHTML = itemTypes.map(t => `
|
||||
// 색상 값 검증 (hex color만 허용)
|
||||
const isValidColor = (color) => /^#[0-9A-Fa-f]{3,6}$/.test(color);
|
||||
container.innerHTML = itemTypes.map(t => {
|
||||
const safeColor = isValidColor(t.color) ? t.color : '#888888';
|
||||
return `
|
||||
<div class="item-legend-item">
|
||||
<div class="item-legend-icon" style="background: ${t.color}20; border: 1px solid ${t.color};">
|
||||
${t.icon}
|
||||
<div class="item-legend-icon" style="background: ${safeColor}20; border: 1px solid ${safeColor};">
|
||||
${escapeHtml(t.icon)}
|
||||
</div>
|
||||
<span>${t.type_name}</span>
|
||||
<span>${escapeHtml(t.type_name)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 편집 모드 토글
|
||||
|
||||
@@ -843,22 +843,22 @@ window.addManualWorkRow = function() {
|
||||
<td>
|
||||
<select class="form-input-compact" id="worker_${manualIndex}" style="width: 120px;" required>
|
||||
<option value="">작업자 선택</option>
|
||||
${workers.map(w => `<option value="${w.worker_id}">${w.worker_name} (${w.job_type || '-'})</option>`).join('')}
|
||||
${workers.map(w => `<option value="${escapeHtml(String(w.worker_id))}">${escapeHtml(w.worker_name)} (${escapeHtml(w.job_type || '-')})</option>`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="date" class="form-input-compact" id="date_${manualIndex}" value="${getKoreaToday()}" required style="width: 130px;">
|
||||
<input type="date" class="form-input-compact" id="date_${manualIndex}" value="${escapeHtml(getKoreaToday())}" required style="width: 130px;">
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-input-compact" id="project_${manualIndex}" style="width: 120px;" required>
|
||||
<option value="">프로젝트 선택</option>
|
||||
${projects.map(p => `<option value="${p.project_id}">${p.project_name}</option>`).join('')}
|
||||
${projects.map(p => `<option value="${escapeHtml(String(p.project_id))}">${escapeHtml(p.project_name)}</option>`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-input-compact" id="workType_${manualIndex}" style="width: 120px;" required onchange="loadTasksForWorkType('${manualIndex}')">
|
||||
<option value="">공정 선택</option>
|
||||
${workTypes.map(wt => `<option value="${wt.id}">${wt.name}</option>`).join('')}
|
||||
${workTypes.map(wt => `<option value="${escapeHtml(String(wt.id))}">${escapeHtml(wt.name)}</option>`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
@@ -969,7 +969,7 @@ window.loadTasksForWorkType = async function(manualIndex) {
|
||||
taskSelect.disabled = false;
|
||||
taskSelect.innerHTML = `
|
||||
<option value="">작업 선택</option>
|
||||
${tasks.map(task => `<option value="${task.task_id}">${task.task_name}</option>`).join('')}
|
||||
${tasks.map(task => `<option value="${escapeHtml(String(task.task_id))}">${escapeHtml(task.task_name)}</option>`).join('')}
|
||||
`;
|
||||
} else {
|
||||
taskSelect.disabled = true;
|
||||
@@ -1022,12 +1022,17 @@ window.openWorkplaceMapForManual = async function(manualIndex) {
|
||||
const modal = document.getElementById('workplaceModal');
|
||||
const categoryList = document.getElementById('workplaceCategoryList');
|
||||
|
||||
categoryList.innerHTML = categories.map(cat => `
|
||||
<button type="button" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceCategory(${cat.category_id}, "${cat.category_name.replace(/"/g, '"')}", "${(cat.layout_image || '').replace(/"/g, '"')}")'>
|
||||
categoryList.innerHTML = categories.map(cat => {
|
||||
const safeId = parseInt(cat.category_id) || 0;
|
||||
const safeName = escapeHtml(cat.category_name);
|
||||
const safeImage = escapeHtml(cat.layout_image || '');
|
||||
return `
|
||||
<button type="button" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceCategory(${safeId}, "${safeName.replace(/"/g, '"')}", "${safeImage.replace(/"/g, '"')}")'>
|
||||
<span style="margin-right: 0.5rem;">🏭</span>
|
||||
${cat.category_name}
|
||||
${safeName}
|
||||
</button>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// 카테고리 선택 화면 표시
|
||||
document.getElementById('categorySelectionArea').style.display = 'block';
|
||||
@@ -1071,12 +1076,16 @@ window.selectWorkplaceCategory = async function(categoryId, categoryName, layout
|
||||
|
||||
// 리스트 항상 표시
|
||||
const workplaceListArea = document.getElementById('workplaceListArea');
|
||||
workplaceListArea.innerHTML = workplaces.map(wp => `
|
||||
<button type="button" id="workplace-${wp.workplace_id}" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceFromList(${wp.workplace_id}, "${wp.workplace_name.replace(/"/g, '"')}")'>
|
||||
workplaceListArea.innerHTML = workplaces.map(wp => {
|
||||
const safeId = parseInt(wp.workplace_id) || 0;
|
||||
const safeName = escapeHtml(wp.workplace_name);
|
||||
return `
|
||||
<button type="button" id="workplace-${safeId}" class="btn btn-secondary" style="width: 100%; text-align: left;" onclick='selectWorkplaceFromList(${safeId}, "${safeName.replace(/"/g, '"')}")'>
|
||||
<span style="margin-right: 0.5rem;">📍</span>
|
||||
${wp.workplace_name}
|
||||
${safeName}
|
||||
</button>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업장소 로드 오류:', error);
|
||||
@@ -1270,8 +1279,8 @@ window.confirmWorkplaceSelection = function() {
|
||||
<span>작업장소 선택됨</span>
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: #111827; font-weight: 500;">
|
||||
<div style="color: #6b7280; font-size: 0.7rem; margin-bottom: 2px;">🏭 ${selectedWorkplaceCategoryName}</div>
|
||||
<div>📍 ${selectedWorkplaceName}</div>
|
||||
<div style="color: #6b7280; font-size: 0.7rem; margin-bottom: 2px;">🏭 ${escapeHtml(selectedWorkplaceCategoryName)}</div>
|
||||
<div>📍 ${escapeHtml(selectedWorkplaceName)}</div>
|
||||
</div>
|
||||
`;
|
||||
displayDiv.style.background = '#ecfdf5';
|
||||
@@ -1482,49 +1491,49 @@ function renderCompletedReports(reports) {
|
||||
<div class="completed-report-card">
|
||||
<div class="report-header">
|
||||
<div>
|
||||
<h4>${report.worker_name || '작업자'}</h4>
|
||||
<h4>${escapeHtml(report.worker_name || '작업자')}</h4>
|
||||
${report.tbm_session_id ? '<span class="tbm-badge">TBM 연동</span>' : '<span class="manual-badge">수동 입력</span>'}
|
||||
</div>
|
||||
<span class="report-date">${formatDate(report.report_date)}</span>
|
||||
<span class="report-date">${escapeHtml(formatDate(report.report_date))}</span>
|
||||
</div>
|
||||
|
||||
<div class="report-info">
|
||||
<div class="info-row">
|
||||
<span class="label">프로젝트:</span>
|
||||
<span class="value">${report.project_name || '-'}</span>
|
||||
<span class="value">${escapeHtml(report.project_name || '-')}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">공정:</span>
|
||||
<span class="value">${report.work_type_name || '-'}</span>
|
||||
<span class="value">${escapeHtml(report.work_type_name || '-')}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">작업시간:</span>
|
||||
<span class="value">${report.total_hours || report.work_hours || 0}시간</span>
|
||||
<span class="value">${parseFloat(report.total_hours || report.work_hours || 0)}시간</span>
|
||||
</div>
|
||||
${report.regular_hours !== undefined && report.regular_hours !== null ? `
|
||||
<div class="info-row">
|
||||
<span class="label">정규 시간:</span>
|
||||
<span class="value">${report.regular_hours}시간</span>
|
||||
<span class="value">${parseFloat(report.regular_hours)}시간</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${report.error_hours && report.error_hours > 0 ? `
|
||||
<div class="info-row">
|
||||
<span class="label">부적합 처리:</span>
|
||||
<span class="value" style="color: #dc2626;">${report.error_hours}시간</span>
|
||||
<span class="value" style="color: #dc2626;">${parseFloat(report.error_hours)}시간</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">부적합 원인:</span>
|
||||
<span class="value">${report.error_type_name || '-'}</span>
|
||||
<span class="value">${escapeHtml(report.error_type_name || '-')}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="info-row">
|
||||
<span class="label">작성자:</span>
|
||||
<span class="value">${report.created_by_name || '-'}</span>
|
||||
<span class="value">${escapeHtml(report.created_by_name || '-')}</span>
|
||||
</div>
|
||||
${report.start_time && report.end_time ? `
|
||||
<div class="info-row">
|
||||
<span class="label">작업 시간:</span>
|
||||
<span class="value">${report.start_time} ~ ${report.end_time}</span>
|
||||
<span class="value">${escapeHtml(report.start_time)} ~ ${escapeHtml(report.end_time)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -1972,10 +1981,10 @@ function addWorkEntry() {
|
||||
</div>
|
||||
<select class="form-select project-select" required>
|
||||
<option value="">프로젝트를 선택하세요</option>
|
||||
${projects.map(p => `<option value="${p.project_id}">${p.project_name}</option>`).join('')}
|
||||
${projects.map(p => `<option value="${escapeHtml(String(p.project_id))}">${escapeHtml(p.project_name)}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-field-group">
|
||||
<div class="form-field-label">
|
||||
<span class="form-field-icon">⚙️</span>
|
||||
@@ -1983,7 +1992,7 @@ function addWorkEntry() {
|
||||
</div>
|
||||
<select class="form-select work-type-select" required>
|
||||
<option value="">작업 유형을 선택하세요</option>
|
||||
${workTypes.map(wt => `<option value="${wt.id}">${wt.name}</option>`).join('')}
|
||||
${workTypes.map(wt => `<option value="${escapeHtml(String(wt.id))}">${escapeHtml(wt.name)}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1996,7 +2005,7 @@ function addWorkEntry() {
|
||||
</div>
|
||||
<select class="form-select work-status-select" required>
|
||||
<option value="">업무 상태를 선택하세요</option>
|
||||
${workStatusTypes.map(ws => `<option value="${ws.id}">${ws.name}</option>`).join('')}
|
||||
${workStatusTypes.map(ws => `<option value="${escapeHtml(String(ws.id))}">${escapeHtml(ws.name)}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2422,7 +2431,7 @@ function displayMyDailyWorkers(data, date) {
|
||||
|
||||
const headerHtml = `
|
||||
<div class="daily-workers-header">
|
||||
<h4>📊 내가 입력한 오늘(${date}) 작업 현황 - 총 ${totalWorkers}명, ${totalWorks}개 작업</h4>
|
||||
<h4>📊 내가 입력한 오늘(${escapeHtml(date)}) 작업 현황 - 총 ${parseInt(totalWorkers) || 0}명, ${parseInt(totalWorks) || 0}개 작업</h4>
|
||||
<button class="refresh-btn" onclick="refreshTodayWorkers()">
|
||||
🔄 새로고침
|
||||
</button>
|
||||
@@ -2436,12 +2445,12 @@ function displayMyDailyWorkers(data, date) {
|
||||
|
||||
// 개별 작업 항목들 (수정/삭제 버튼 포함)
|
||||
const individualWorksHtml = works.map((work) => {
|
||||
const projectName = work.project_name || '미지정';
|
||||
const workTypeName = work.work_type_name || '미지정';
|
||||
const workStatusName = work.work_status_name || '미지정';
|
||||
const workHours = work.work_hours || 0;
|
||||
const errorTypeName = work.error_type_name || null;
|
||||
const workId = work.id;
|
||||
const projectName = escapeHtml(work.project_name || '미지정');
|
||||
const workTypeName = escapeHtml(work.work_type_name || '미지정');
|
||||
const workStatusName = escapeHtml(work.work_status_name || '미지정');
|
||||
const workHours = parseFloat(work.work_hours || 0);
|
||||
const errorTypeName = work.error_type_name ? escapeHtml(work.error_type_name) : null;
|
||||
const workId = parseInt(work.id) || 0;
|
||||
|
||||
return `
|
||||
<div class="individual-work-item">
|
||||
@@ -2484,8 +2493,8 @@ function displayMyDailyWorkers(data, date) {
|
||||
return `
|
||||
<div class="worker-status-item">
|
||||
<div class="worker-header">
|
||||
<div class="worker-name">👤 ${workerName}</div>
|
||||
<div class="worker-total-hours">총 ${totalHours}시간</div>
|
||||
<div class="worker-name">👤 ${escapeHtml(workerName)}</div>
|
||||
<div class="worker-total-hours">총 ${parseFloat(totalHours)}시간</div>
|
||||
</div>
|
||||
<div class="individual-works-container">
|
||||
${individualWorksHtml}
|
||||
@@ -2981,8 +2990,8 @@ function renderInlineDefectList(index) {
|
||||
let html = `
|
||||
<div class="defect-issue-section">
|
||||
<div class="defect-issue-header">
|
||||
<span class="defect-issue-title">📋 ${workerWorkplaceName || '작업장소'} 관련 부적합</span>
|
||||
<span class="defect-issue-count">${nonconformityIssues.length}건</span>
|
||||
<span class="defect-issue-title">📋 ${escapeHtml(workerWorkplaceName || '작업장소')} 관련 부적합</span>
|
||||
<span class="defect-issue-count">${parseInt(nonconformityIssues.length) || 0}건</span>
|
||||
</div>
|
||||
<div class="defect-issue-list">
|
||||
`;
|
||||
@@ -2999,23 +3008,24 @@ function renderInlineDefectList(index) {
|
||||
itemText = itemText ? `${itemText} - ${issue.additional_description}` : issue.additional_description;
|
||||
}
|
||||
|
||||
const safeReportId = parseInt(issue.report_id) || 0;
|
||||
html += `
|
||||
<div class="defect-issue-item ${isSelected ? 'selected' : ''}" data-issue-id="${issue.report_id}">
|
||||
<div class="defect-issue-item ${isSelected ? 'selected' : ''}" data-issue-id="${safeReportId}">
|
||||
<div class="defect-issue-checkbox">
|
||||
<input type="checkbox"
|
||||
id="issueCheck_${index}_${issue.report_id}"
|
||||
id="issueCheck_${index}_${safeReportId}"
|
||||
${isSelected ? 'checked' : ''}
|
||||
onchange="toggleIssueDefect('${index}', ${issue.report_id}, this.checked)">
|
||||
onchange="toggleIssueDefect('${index}', ${safeReportId}, this.checked)">
|
||||
</div>
|
||||
<div class="defect-issue-info">
|
||||
<span class="defect-issue-category">${issue.issue_category_name || '부적합'}</span>
|
||||
<span class="defect-issue-item-name">${itemText || '-'}</span>
|
||||
<span class="defect-issue-location">${issue.workplace_name || issue.custom_location || ''}</span>
|
||||
<span class="defect-issue-category">${escapeHtml(issue.issue_category_name || '부적합')}</span>
|
||||
<span class="defect-issue-item-name">${escapeHtml(itemText || '-')}</span>
|
||||
<span class="defect-issue-location">${escapeHtml(issue.workplace_name || issue.custom_location || '')}</span>
|
||||
</div>
|
||||
<div class="defect-issue-time ${isSelected ? 'active' : ''}">
|
||||
<div class="defect-time-input ${isSelected ? '' : 'disabled'}"
|
||||
onclick="${isSelected ? `openIssueDefectTimePicker('${index}', ${issue.report_id})` : ''}">
|
||||
<span class="defect-time-value" id="issueDefectTime_${index}_${issue.report_id}">${defectHours}</span>
|
||||
onclick="${isSelected ? `openIssueDefectTimePicker('${index}', ${safeReportId})` : ''}">
|
||||
<span class="defect-time-value" id="issueDefectTime_${index}_${safeReportId}">${parseFloat(defectHours) || 0}</span>
|
||||
<span class="defect-time-unit">시간</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3052,7 +3062,7 @@ function renderInlineDefectList(index) {
|
||||
} else {
|
||||
// 이슈가 없으면 레거시 UI (error_types 선택)
|
||||
const noIssueMsg = workerWorkplaceName
|
||||
? `${workerWorkplaceName}에 신고된 부적합이 없습니다.`
|
||||
? `${escapeHtml(workerWorkplaceName)}에 신고된 부적합이 없습니다.`
|
||||
: '신고된 부적합이 없습니다.';
|
||||
listContainer.innerHTML = `
|
||||
<div class="defect-no-issues">
|
||||
|
||||
@@ -99,27 +99,27 @@ function renderEquipmentInfo() {
|
||||
<div class="eq-info-grid">
|
||||
<div class="eq-info-item">
|
||||
<span class="eq-info-label">관리번호</span>
|
||||
<span class="eq-info-value">${eq.equipment_code}</span>
|
||||
<span class="eq-info-value">${escapeHtml(eq.equipment_code || '-')}</span>
|
||||
</div>
|
||||
<div class="eq-info-item">
|
||||
<span class="eq-info-label">설비명</span>
|
||||
<span class="eq-info-value">${eq.equipment_name}</span>
|
||||
<span class="eq-info-value">${escapeHtml(eq.equipment_name || '-')}</span>
|
||||
</div>
|
||||
<div class="eq-info-item">
|
||||
<span class="eq-info-label">모델명</span>
|
||||
<span class="eq-info-value">${eq.model_name || '-'}</span>
|
||||
<span class="eq-info-value">${escapeHtml(eq.model_name || '-')}</span>
|
||||
</div>
|
||||
<div class="eq-info-item">
|
||||
<span class="eq-info-label">규격</span>
|
||||
<span class="eq-info-value">${eq.specifications || '-'}</span>
|
||||
<span class="eq-info-value">${escapeHtml(eq.specifications || '-')}</span>
|
||||
</div>
|
||||
<div class="eq-info-item">
|
||||
<span class="eq-info-label">제조사</span>
|
||||
<span class="eq-info-value">${eq.manufacturer || '-'}</span>
|
||||
<span class="eq-info-value">${escapeHtml(eq.manufacturer || '-')}</span>
|
||||
</div>
|
||||
<div class="eq-info-item">
|
||||
<span class="eq-info-label">구입처</span>
|
||||
<span class="eq-info-value">${eq.supplier || '-'}</span>
|
||||
<span class="eq-info-value">${escapeHtml(eq.supplier || '-')}</span>
|
||||
</div>
|
||||
<div class="eq-info-item">
|
||||
<span class="eq-info-label">구입일</span>
|
||||
@@ -131,11 +131,11 @@ function renderEquipmentInfo() {
|
||||
</div>
|
||||
<div class="eq-info-item">
|
||||
<span class="eq-info-label">시리얼번호</span>
|
||||
<span class="eq-info-value">${eq.serial_number || '-'}</span>
|
||||
<span class="eq-info-value">${escapeHtml(eq.serial_number || '-')}</span>
|
||||
</div>
|
||||
<div class="eq-info-item">
|
||||
<span class="eq-info-label">설비유형</span>
|
||||
<span class="eq-info-value">${eq.equipment_type || '-'}</span>
|
||||
<span class="eq-info-value">${escapeHtml(eq.equipment_type || '-')}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -219,12 +219,17 @@ function renderPhotos(photos) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = photos.map(photo => `
|
||||
<div class="eq-photo-item" onclick="viewPhoto('${window.API_BASE_URL}${photo.photo_path}')">
|
||||
<img src="${window.API_BASE_URL}${photo.photo_path}" alt="${photo.description || '설비 사진'}">
|
||||
<button class="eq-photo-delete" onclick="event.stopPropagation(); deletePhoto(${photo.photo_id})">×</button>
|
||||
</div>
|
||||
`).join('');
|
||||
grid.innerHTML = photos.map(photo => {
|
||||
const safePhotoId = parseInt(photo.photo_id) || 0;
|
||||
const safePhotoPath = encodeURI(photo.photo_path || '');
|
||||
const safeDescription = escapeHtml(photo.description || '설비 사진');
|
||||
return `
|
||||
<div class="eq-photo-item" onclick="viewPhoto('${window.API_BASE_URL}${safePhotoPath}')">
|
||||
<img src="${window.API_BASE_URL}${safePhotoPath}" alt="${safeDescription}">
|
||||
<button class="eq-photo-delete" onclick="event.stopPropagation(); deletePhoto(${safePhotoId})">×</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function openPhotoModal() {
|
||||
@@ -323,7 +328,8 @@ function openMoveModal() {
|
||||
const factorySelect = document.getElementById('moveFactorySelect');
|
||||
factorySelect.innerHTML = '<option value="">공장을 선택하세요</option>';
|
||||
factories.forEach(f => {
|
||||
factorySelect.innerHTML += `<option value="${f.category_id}">${f.category_name}</option>`;
|
||||
const safeCategoryId = parseInt(f.category_id) || 0;
|
||||
factorySelect.innerHTML += `<option value="${safeCategoryId}">${escapeHtml(f.category_name || '-')}</option>`;
|
||||
});
|
||||
|
||||
document.getElementById('moveWorkplaceSelect').innerHTML = '<option value="">작업장을 선택하세요</option>';
|
||||
@@ -354,7 +360,8 @@ async function loadMoveWorkplaces() {
|
||||
workplaces = response.data.data;
|
||||
workplaces.forEach(wp => {
|
||||
if (wp.map_image_url) {
|
||||
workplaceSelect.innerHTML += `<option value="${wp.workplace_id}">${wp.workplace_name}</option>`;
|
||||
const safeWorkplaceId = parseInt(wp.workplace_id) || 0;
|
||||
workplaceSelect.innerHTML += `<option value="${safeWorkplaceId}">${escapeHtml(wp.workplace_name || '-')}</option>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -475,7 +482,8 @@ function openRepairModal() {
|
||||
const select = document.getElementById('repairItemSelect');
|
||||
select.innerHTML = '<option value="">선택하세요</option>';
|
||||
repairCategories.forEach(item => {
|
||||
select.innerHTML += `<option value="${item.item_id}">${item.item_name}</option>`;
|
||||
const safeItemId = parseInt(item.item_id) || 0;
|
||||
select.innerHTML += `<option value="${safeItemId}">${escapeHtml(item.item_name || '-')}</option>`;
|
||||
});
|
||||
|
||||
document.getElementById('repairDescription').value = '';
|
||||
@@ -557,16 +565,20 @@ function renderRepairHistory(history) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = history.map(h => `
|
||||
<div class="eq-history-item">
|
||||
<span class="eq-history-date">${formatDate(h.created_at)}</span>
|
||||
<div class="eq-history-content">
|
||||
<div class="eq-history-title">${h.item_name || '수리 요청'}</div>
|
||||
<div class="eq-history-detail">${h.description || '-'}</div>
|
||||
const validStatuses = ['pending', 'in_progress', 'completed', 'closed'];
|
||||
container.innerHTML = history.map(h => {
|
||||
const safeStatus = validStatuses.includes(h.status) ? h.status : 'pending';
|
||||
return `
|
||||
<div class="eq-history-item">
|
||||
<span class="eq-history-date">${formatDate(h.created_at)}</span>
|
||||
<div class="eq-history-content">
|
||||
<div class="eq-history-title">${escapeHtml(h.item_name || '수리 요청')}</div>
|
||||
<div class="eq-history-detail">${escapeHtml(h.description || '-')}</div>
|
||||
</div>
|
||||
<span class="eq-history-status ${safeStatus}">${getRepairStatusLabel(h.status)}</span>
|
||||
</div>
|
||||
<span class="eq-history-status ${h.status}">${getRepairStatusLabel(h.status)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getRepairStatusLabel(status) {
|
||||
@@ -664,16 +676,17 @@ function renderExternalLogs(logs) {
|
||||
const isReturned = !!log.actual_return_date;
|
||||
const statusClass = isReturned ? 'returned' : 'exported';
|
||||
const statusLabel = isReturned ? '반입완료' : '반출중';
|
||||
const safeLogId = parseInt(log.log_id) || 0;
|
||||
|
||||
return `
|
||||
<div class="eq-history-item">
|
||||
<span class="eq-history-date">${dateRange}</span>
|
||||
<div class="eq-history-content">
|
||||
<div class="eq-history-title">${log.destination || '외부'}</div>
|
||||
<div class="eq-history-detail">${log.reason || '-'}</div>
|
||||
<div class="eq-history-title">${escapeHtml(log.destination || '외부')}</div>
|
||||
<div class="eq-history-detail">${escapeHtml(log.reason || '-')}</div>
|
||||
</div>
|
||||
<span class="eq-history-status ${statusClass}">${statusLabel}</span>
|
||||
${!isReturned ? `<button class="eq-history-action" onclick="openReturnModal(${log.log_id})">반입처리</button>` : ''}
|
||||
${!isReturned ? `<button class="eq-history-action" onclick="openReturnModal(${safeLogId})">반입처리</button>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
@@ -748,15 +761,15 @@ function renderMoveLogs(logs) {
|
||||
container.innerHTML = logs.map(log => {
|
||||
const typeLabel = log.move_type === 'temporary' ? '임시이동' : '복귀';
|
||||
const location = log.move_type === 'temporary'
|
||||
? `${log.to_workplace_name || '-'}`
|
||||
: `원위치 복귀`;
|
||||
? escapeHtml(log.to_workplace_name || '-')
|
||||
: '원위치 복귀';
|
||||
|
||||
return `
|
||||
<div class="eq-history-item">
|
||||
<span class="eq-history-date">${formatDateTime(log.moved_at)}</span>
|
||||
<div class="eq-history-content">
|
||||
<div class="eq-history-title">${typeLabel}: ${location}</div>
|
||||
<div class="eq-history-detail">${log.reason || '-'} (${log.moved_by_name || '시스템'})</div>
|
||||
<div class="eq-history-detail">${escapeHtml(log.reason || '-')} (${escapeHtml(log.moved_by_name || '시스템')})</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -135,9 +135,13 @@ function populateWorkplaceFilters() {
|
||||
const filterWorkplace = document.getElementById('filterWorkplace');
|
||||
const modalWorkplace = document.getElementById('workplaceId');
|
||||
|
||||
const workplaceOptions = workplaces.map(w =>
|
||||
`<option value="${w.workplace_id}">${w.category_name ? w.category_name + ' - ' : ''}${w.workplace_name}</option>`
|
||||
).join('');
|
||||
const workplaceOptions = workplaces.map(w => {
|
||||
const safeId = parseInt(w.workplace_id) || 0;
|
||||
const categoryName = escapeHtml(w.category_name || '');
|
||||
const workplaceName = escapeHtml(w.workplace_name || '');
|
||||
const label = categoryName ? categoryName + ' - ' + workplaceName : workplaceName;
|
||||
return `<option value="${safeId}">${label}</option>`;
|
||||
}).join('');
|
||||
|
||||
if (filterWorkplace) filterWorkplace.innerHTML = '<option value="">전체</option>' + workplaceOptions;
|
||||
if (modalWorkplace) modalWorkplace.innerHTML = '<option value="">선택 안함</option>' + workplaceOptions;
|
||||
@@ -148,9 +152,10 @@ function populateTypeFilter() {
|
||||
const filterType = document.getElementById('filterType');
|
||||
if (!filterType) return;
|
||||
|
||||
const typeOptions = equipmentTypes.map(type =>
|
||||
`<option value="${type}">${type}</option>`
|
||||
).join('');
|
||||
const typeOptions = equipmentTypes.map(type => {
|
||||
const safeType = escapeHtml(type || '');
|
||||
return `<option value="${safeType}">${safeType}</option>`;
|
||||
}).join('');
|
||||
filterType.innerHTML = '<option value="">전체</option>' + typeOptions;
|
||||
}
|
||||
|
||||
@@ -189,33 +194,44 @@ function renderEquipmentList() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${equipments.map(eq => `
|
||||
<tr>
|
||||
<td class="eq-col-code">${eq.equipment_code || '-'}</td>
|
||||
<td class="eq-col-name" title="${eq.equipment_name || ''}">${eq.equipment_name || '-'}</td>
|
||||
<td class="eq-col-model" title="${eq.model_name || ''}">${eq.model_name || '-'}</td>
|
||||
<td class="eq-col-spec" title="${eq.specifications || ''}">${eq.specifications || '-'}</td>
|
||||
<td>${eq.manufacturer || '-'}</td>
|
||||
<td>${eq.supplier || '-'}</td>
|
||||
<td class="eq-col-price">${eq.purchase_price ? formatPrice(eq.purchase_price) : '-'}</td>
|
||||
<td class="eq-col-date">${eq.installation_date ? formatDate(eq.installation_date) : '-'}</td>
|
||||
<td>
|
||||
<span class="eq-status eq-status-${eq.status}">
|
||||
${getStatusText(eq.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="eq-actions">
|
||||
<button class="eq-btn-action eq-btn-edit" onclick="editEquipment(${eq.equipment_id})" title="수정">
|
||||
✏️
|
||||
</button>
|
||||
<button class="eq-btn-action eq-btn-delete" onclick="deleteEquipment(${eq.equipment_id})" title="삭제">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
${equipments.map(eq => {
|
||||
const safeId = parseInt(eq.equipment_id) || 0;
|
||||
const safeCode = escapeHtml(eq.equipment_code || '-');
|
||||
const safeName = escapeHtml(eq.equipment_name || '-');
|
||||
const safeModel = escapeHtml(eq.model_name || '-');
|
||||
const safeSpec = escapeHtml(eq.specifications || '-');
|
||||
const safeManufacturer = escapeHtml(eq.manufacturer || '-');
|
||||
const safeSupplier = escapeHtml(eq.supplier || '-');
|
||||
const validStatuses = ['active', 'maintenance', 'inactive'];
|
||||
const safeStatus = validStatuses.includes(eq.status) ? eq.status : 'inactive';
|
||||
return `
|
||||
<tr>
|
||||
<td class="eq-col-code">${safeCode}</td>
|
||||
<td class="eq-col-name" title="${safeName}">${safeName}</td>
|
||||
<td class="eq-col-model" title="${safeModel}">${safeModel}</td>
|
||||
<td class="eq-col-spec" title="${safeSpec}">${safeSpec}</td>
|
||||
<td>${safeManufacturer}</td>
|
||||
<td>${safeSupplier}</td>
|
||||
<td class="eq-col-price">${eq.purchase_price ? formatPrice(eq.purchase_price) : '-'}</td>
|
||||
<td class="eq-col-date">${eq.installation_date ? formatDate(eq.installation_date) : '-'}</td>
|
||||
<td>
|
||||
<span class="eq-status eq-status-${safeStatus}">
|
||||
${getStatusText(eq.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="eq-actions">
|
||||
<button class="eq-btn-action eq-btn-edit" onclick="editEquipment(${safeId})" title="수정">
|
||||
✏️
|
||||
</button>
|
||||
<button class="eq-btn-action eq-btn-delete" onclick="deleteEquipment(${safeId})" title="삭제">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -146,11 +146,17 @@ function renderBasicInfo(d) {
|
||||
});
|
||||
};
|
||||
|
||||
const validTypes = ['nonconformity', 'safety'];
|
||||
const safeType = validTypes.includes(d.category_type) ? d.category_type : '';
|
||||
const reporterName = escapeHtml(d.reporter_full_name || d.reporter_name || '-');
|
||||
const locationText = escapeHtml(d.custom_location || d.workplace_name || '-');
|
||||
const factoryText = d.factory_name ? ` (${escapeHtml(d.factory_name)})` : '';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="info-item">
|
||||
<div class="info-label">신고 유형</div>
|
||||
<div class="info-value">
|
||||
<span class="type-badge ${d.category_type}">${typeNames[d.category_type] || d.category_type}</span>
|
||||
<span class="type-badge ${safeType}">${typeNames[d.category_type] || escapeHtml(d.category_type || '-')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
@@ -159,11 +165,11 @@ function renderBasicInfo(d) {
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">신고자</div>
|
||||
<div class="info-value">${d.reporter_full_name || d.reporter_name || '-'}</div>
|
||||
<div class="info-value">${reporterName}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">위치</div>
|
||||
<div class="info-value">${d.custom_location || d.workplace_name || '-'}${d.factory_name ? ` (${d.factory_name})` : ''}</div>
|
||||
<div class="info-value">${locationText}${factoryText}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -174,17 +180,20 @@ function renderBasicInfo(d) {
|
||||
function renderIssueContent(d) {
|
||||
const container = document.getElementById('issueContent');
|
||||
|
||||
const validSeverities = ['critical', 'high', 'medium', 'low'];
|
||||
const safeSeverity = validSeverities.includes(d.severity) ? d.severity : '';
|
||||
|
||||
let html = `
|
||||
<div class="info-grid" style="margin-bottom: 1rem;">
|
||||
<div class="info-item">
|
||||
<div class="info-label">카테고리</div>
|
||||
<div class="info-value">${d.issue_category_name || '-'}</div>
|
||||
<div class="info-value">${escapeHtml(d.issue_category_name || '-')}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">항목</div>
|
||||
<div class="info-value">
|
||||
${d.issue_item_name || '-'}
|
||||
${d.severity ? `<span class="severity-badge ${d.severity}">${severityNames[d.severity]}</span>` : ''}
|
||||
${escapeHtml(d.issue_item_name || '-')}
|
||||
${d.severity ? `<span class="severity-badge ${safeSeverity}">${severityNames[d.severity] || escapeHtml(d.severity)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,11 +271,11 @@ function renderProcessInfo(d) {
|
||||
html += `
|
||||
<div class="info-item">
|
||||
<div class="info-label">담당자</div>
|
||||
<div class="info-value">${d.assigned_full_name || d.assigned_user_name || '-'}</div>
|
||||
<div class="info-value">${escapeHtml(d.assigned_full_name || d.assigned_user_name || '-')}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">담당 부서</div>
|
||||
<div class="info-value">${d.assigned_department || '-'}</div>
|
||||
<div class="info-value">${escapeHtml(d.assigned_department || '-')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -279,7 +288,7 @@ function renderProcessInfo(d) {
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">처리자</div>
|
||||
<div class="info-value">${d.resolved_by_name || '-'}</div>
|
||||
<div class="info-value">${escapeHtml(d.resolved_by_name || '-')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -402,10 +411,10 @@ function renderStatusTimeline(logs) {
|
||||
container.innerHTML = logs.map(log => `
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-status">
|
||||
${log.previous_status ? `${statusNames[log.previous_status]} → ` : ''}${statusNames[log.new_status]}
|
||||
${log.previous_status ? `${statusNames[log.previous_status] || escapeHtml(log.previous_status)} → ` : ''}${statusNames[log.new_status] || escapeHtml(log.new_status)}
|
||||
</div>
|
||||
<div class="timeline-meta">
|
||||
${log.changed_by_full_name || log.changed_by_name} | ${formatDate(log.changed_at)}
|
||||
${escapeHtml(log.changed_by_full_name || log.changed_by_name || '-')} | ${formatDate(log.changed_at)}
|
||||
${log.change_reason ? `<br><small>${escapeHtml(log.change_reason)}</small>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@@ -530,7 +539,8 @@ async function openAssignModal() {
|
||||
|
||||
if (data.success && data.data) {
|
||||
data.data.forEach(user => {
|
||||
select.innerHTML += `<option value="${user.user_id}">${user.name} (${user.username})</option>`;
|
||||
const safeUserId = parseInt(user.user_id) || 0;
|
||||
select.innerHTML += `<option value="${safeUserId}">${escapeHtml(user.name || '-')} (${escapeHtml(user.username || '-')})</option>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,9 +490,12 @@ function showWorkSelectionModal(workers, visitors) {
|
||||
workers.forEach(w => {
|
||||
const option = document.createElement('div');
|
||||
option.className = 'work-option';
|
||||
const safeTaskName = escapeHtml(w.task_name || '작업');
|
||||
const safeProjectName = escapeHtml(w.project_name || '');
|
||||
const memberCount = parseInt(w.member_count) || 0;
|
||||
option.innerHTML = `
|
||||
<div class="work-option-title">TBM: ${w.task_name || '작업'}</div>
|
||||
<div class="work-option-desc">${w.project_name || ''} - ${w.member_count || 0}명</div>
|
||||
<div class="work-option-title">TBM: ${safeTaskName}</div>
|
||||
<div class="work-option-desc">${safeProjectName} - ${memberCount}명</div>
|
||||
`;
|
||||
option.onclick = () => {
|
||||
selectedTbmSessionId = w.session_id;
|
||||
@@ -507,9 +510,12 @@ function showWorkSelectionModal(workers, visitors) {
|
||||
visitors.forEach(v => {
|
||||
const option = document.createElement('div');
|
||||
option.className = 'work-option';
|
||||
const safeCompany = escapeHtml(v.visitor_company || '-');
|
||||
const safePurpose = escapeHtml(v.purpose_name || '방문');
|
||||
const visitorCount = parseInt(v.visitor_count) || 0;
|
||||
option.innerHTML = `
|
||||
<div class="work-option-title">출입: ${v.visitor_company}</div>
|
||||
<div class="work-option-desc">${v.purpose_name || '방문'} - ${v.visitor_count || 0}명</div>
|
||||
<div class="work-option-title">출입: ${safeCompany}</div>
|
||||
<div class="work-option-desc">${safePurpose} - ${visitorCount}명</div>
|
||||
`;
|
||||
option.onclick = () => {
|
||||
selectedVisitRequestId = v.request_id;
|
||||
@@ -540,20 +546,20 @@ function updateLocationInfo() {
|
||||
|
||||
if (useCustom && customLocation) {
|
||||
infoBox.classList.remove('empty');
|
||||
infoBox.innerHTML = `<strong>선택된 위치:</strong> ${customLocation}`;
|
||||
infoBox.innerHTML = `<strong>선택된 위치:</strong> ${escapeHtml(customLocation)}`;
|
||||
} else if (selectedWorkplaceName) {
|
||||
infoBox.classList.remove('empty');
|
||||
let html = `<strong>선택된 위치:</strong> ${selectedWorkplaceName}`;
|
||||
let html = `<strong>선택된 위치:</strong> ${escapeHtml(selectedWorkplaceName)}`;
|
||||
|
||||
if (selectedTbmSessionId) {
|
||||
const worker = todayWorkers.find(w => w.session_id === selectedTbmSessionId);
|
||||
if (worker) {
|
||||
html += `<br><span style="color: var(--primary-600);">연결 작업: ${worker.task_name} (TBM)</span>`;
|
||||
html += `<br><span style="color: var(--primary-600);">연결 작업: ${escapeHtml(worker.task_name || '-')} (TBM)</span>`;
|
||||
}
|
||||
} else if (selectedVisitRequestId) {
|
||||
const visitor = todayVisitors.find(v => v.request_id === selectedVisitRequestId);
|
||||
if (visitor) {
|
||||
html += `<br><span style="color: var(--primary-600);">연결 작업: ${visitor.visitor_company} (출입)</span>`;
|
||||
html += `<br><span style="color: var(--primary-600);">연결 작업: ${escapeHtml(visitor.visitor_company || '-')} (출입)</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,17 +121,18 @@ function renderIssues(issues) {
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
// 위치 정보
|
||||
let location = issue.custom_location || '';
|
||||
// 위치 정보 (escaped)
|
||||
let location = escapeHtml(issue.custom_location || '');
|
||||
if (issue.factory_name) {
|
||||
location = issue.factory_name;
|
||||
location = escapeHtml(issue.factory_name);
|
||||
if (issue.workplace_name) {
|
||||
location += ` - ${issue.workplace_name}`;
|
||||
location += ` - ${escapeHtml(issue.workplace_name)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 신고 제목 (항목명 또는 카테고리명)
|
||||
const title = issue.issue_item_name || issue.issue_category_name || '부적합 신고';
|
||||
const title = escapeHtml(issue.issue_item_name || issue.issue_category_name || '부적합 신고');
|
||||
const categoryName = escapeHtml(issue.issue_category_name || '부적합');
|
||||
|
||||
// 사진 목록
|
||||
const photos = [
|
||||
@@ -142,15 +143,22 @@ function renderIssues(issues) {
|
||||
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(${issue.report_id})">
|
||||
<div class="issue-card" onclick="viewIssue(${safeReportId})">
|
||||
<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>
|
||||
<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">${issue.issue_category_name || '부적합'}</span>
|
||||
<span class="issue-category-badge">${categoryName}</span>
|
||||
${title}
|
||||
</div>
|
||||
|
||||
@@ -160,7 +168,7 @@ function renderIssues(issues) {
|
||||
<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}
|
||||
${reporterName}
|
||||
</span>
|
||||
<span class="issue-meta-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -180,7 +188,7 @@ function renderIssues(issues) {
|
||||
${location}
|
||||
</span>
|
||||
` : ''}
|
||||
${issue.assigned_full_name ? `
|
||||
${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"/>
|
||||
@@ -188,7 +196,7 @@ function renderIssues(issues) {
|
||||
<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}
|
||||
담당: ${assignedName}
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -196,7 +204,7 @@ function renderIssues(issues) {
|
||||
${photos.length > 0 ? `
|
||||
<div class="issue-photos">
|
||||
${photos.slice(0, 3).map(p => `
|
||||
<img src="${baseUrl}${p}" alt="신고 사진" loading="lazy">
|
||||
<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>
|
||||
|
||||
@@ -165,26 +165,35 @@ function renderProjects() {
|
||||
'completed': { icon: '✅', text: '완료', color: '#3b82f6' },
|
||||
'cancelled': { icon: '❌', text: '취소', color: '#ef4444' }
|
||||
};
|
||||
|
||||
const status = statusMap[project.project_status] || statusMap['active'];
|
||||
|
||||
const validStatuses = ['planning', 'active', 'completed', 'cancelled'];
|
||||
const safeProjectStatus = validStatuses.includes(project.project_status) ? project.project_status : 'active';
|
||||
const status = statusMap[safeProjectStatus];
|
||||
// is_active 값 처리 (DB에서 0/1로 오는 경우 대비)
|
||||
const isInactive = project.is_active === 0 || project.is_active === false || project.is_active === 'false';
|
||||
|
||||
|
||||
// XSS 방지를 위한 안전한 값
|
||||
const safeProjectId = parseInt(project.project_id) || 0;
|
||||
const safeJobNo = escapeHtml(project.job_no || 'Job No. 없음');
|
||||
const safeProjectName = escapeHtml(project.project_name || '-');
|
||||
const safePm = escapeHtml(project.pm || '-');
|
||||
const safeSite = escapeHtml(project.site || '-');
|
||||
|
||||
console.log('🎨 카드 렌더링:', {
|
||||
project_id: project.project_id,
|
||||
project_name: project.project_name,
|
||||
is_active_raw: project.is_active,
|
||||
isInactive: isInactive
|
||||
});
|
||||
|
||||
|
||||
return `
|
||||
<div class="project-card ${isInactive ? 'inactive' : ''}" onclick="editProject(${project.project_id})">
|
||||
<div class="project-card ${isInactive ? 'inactive' : ''}" onclick="editProject(${safeProjectId})">
|
||||
${isInactive ? '<div class="inactive-overlay"><span class="inactive-badge">🚫 비활성화됨</span></div>' : ''}
|
||||
<div class="project-header">
|
||||
<div class="project-info">
|
||||
<div class="project-job-no">${project.job_no || 'Job No. 없음'}</div>
|
||||
<div class="project-job-no">${safeJobNo}</div>
|
||||
<h3 class="project-name">
|
||||
${project.project_name}
|
||||
${safeProjectName}
|
||||
${isInactive ? '<span class="inactive-label">(비활성)</span>' : ''}
|
||||
</h3>
|
||||
<div class="project-meta">
|
||||
@@ -202,20 +211,20 @@ function renderProjects() {
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">PM</span>
|
||||
<span class="meta-value">${project.pm || '-'}</span>
|
||||
<span class="meta-value">${safePm}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">현장</span>
|
||||
<span class="meta-value">${project.site || '-'}</span>
|
||||
<span class="meta-value">${safeSite}</span>
|
||||
</div>
|
||||
${isInactive ? '<div class="inactive-notice">⚠️ 작업보고서에서 숨김</div>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-actions">
|
||||
<button class="btn-edit" onclick="event.stopPropagation(); editProject(${project.project_id})" title="수정">
|
||||
<button class="btn-edit" onclick="event.stopPropagation(); editProject(${safeProjectId})" title="수정">
|
||||
✏️ 수정
|
||||
</button>
|
||||
<button class="btn-delete" onclick="event.stopPropagation(); confirmDeleteProject(${project.project_id})" title="삭제">
|
||||
<button class="btn-delete" onclick="event.stopPropagation(); confirmDeleteProject(${safeProjectId})" title="삭제">
|
||||
🗑️ 삭제
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -121,17 +121,18 @@ function renderIssues(issues) {
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
// 위치 정보
|
||||
let location = issue.custom_location || '';
|
||||
// 위치 정보 (escaped)
|
||||
let location = escapeHtml(issue.custom_location || '');
|
||||
if (issue.factory_name) {
|
||||
location = issue.factory_name;
|
||||
location = escapeHtml(issue.factory_name);
|
||||
if (issue.workplace_name) {
|
||||
location += ` - ${issue.workplace_name}`;
|
||||
location += ` - ${escapeHtml(issue.workplace_name)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 신고 제목 (항목명 또는 카테고리명)
|
||||
const title = issue.issue_item_name || issue.issue_category_name || '안전 신고';
|
||||
const title = escapeHtml(issue.issue_item_name || issue.issue_category_name || '안전 신고');
|
||||
const categoryName = escapeHtml(issue.issue_category_name || '안전');
|
||||
|
||||
// 사진 목록
|
||||
const photos = [
|
||||
@@ -142,15 +143,22 @@ function renderIssues(issues) {
|
||||
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(${issue.report_id})">
|
||||
<div class="issue-card" onclick="viewIssue(${safeReportId})">
|
||||
<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>
|
||||
<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">${issue.issue_category_name || '안전'}</span>
|
||||
<span class="issue-category-badge">${categoryName}</span>
|
||||
${title}
|
||||
</div>
|
||||
|
||||
@@ -160,7 +168,7 @@ function renderIssues(issues) {
|
||||
<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}
|
||||
${reporterName}
|
||||
</span>
|
||||
<span class="issue-meta-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -180,7 +188,7 @@ function renderIssues(issues) {
|
||||
${location}
|
||||
</span>
|
||||
` : ''}
|
||||
${issue.assigned_full_name ? `
|
||||
${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"/>
|
||||
@@ -188,7 +196,7 @@ function renderIssues(issues) {
|
||||
<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}
|
||||
담당: ${assignedName}
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -196,7 +204,7 @@ function renderIssues(issues) {
|
||||
${photos.length > 0 ? `
|
||||
<div class="issue-photos">
|
||||
${photos.slice(0, 3).map(p => `
|
||||
<img src="${baseUrl}${p}" alt="신고 사진" loading="lazy">
|
||||
<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>
|
||||
|
||||
@@ -100,14 +100,15 @@ function renderWorkTypeTabs() {
|
||||
const count = tasks.filter(t => t.work_type_id === workType.id).length;
|
||||
const isActive = currentWorkTypeId === workType.id;
|
||||
|
||||
const safeId = parseInt(workType.id) || 0;
|
||||
tabsHtml += `
|
||||
<button class="tab-btn ${isActive ? 'active' : ''}"
|
||||
data-work-type="${workType.id}"
|
||||
onclick="switchWorkType(${workType.id})"
|
||||
data-work-type="${safeId}"
|
||||
onclick="switchWorkType(${safeId})"
|
||||
style="position: relative; padding-right: 3rem;">
|
||||
<span class="tab-icon">🔧</span>
|
||||
${workType.name} (${count})
|
||||
<span onclick="event.stopPropagation(); editWorkType(${workType.id});"
|
||||
${escapeHtml(workType.name)} (${parseInt(count) || 0})
|
||||
<span onclick="event.stopPropagation(); editWorkType(${safeId});"
|
||||
style="position: absolute; right: 0.5rem; padding: 0.25rem 0.5rem; opacity: 0.7; cursor: pointer; font-size: 0.75rem;"
|
||||
title="공정 수정">
|
||||
✏️
|
||||
@@ -157,35 +158,36 @@ function createTaskCard(task) {
|
||||
const statusBadge = task.is_active
|
||||
? '<span class="badge" style="background: #dcfce7; color: #166534;">활성</span>'
|
||||
: '<span class="badge" style="background: #f3f4f6; color: #6b7280;">비활성</span>';
|
||||
const safeTaskId = parseInt(task.task_id) || 0;
|
||||
|
||||
return `
|
||||
<div class="code-card" onclick="editTask(${task.task_id})">
|
||||
<div class="code-card" onclick="editTask(${safeTaskId})">
|
||||
<div class="code-card-header">
|
||||
<h3 class="code-name">${task.task_name}</h3>
|
||||
<h3 class="code-name">${escapeHtml(task.task_name)}</h3>
|
||||
${statusBadge}
|
||||
</div>
|
||||
|
||||
<div class="code-info">
|
||||
<div class="info-item">
|
||||
<span class="info-label">소속 공정</span>
|
||||
<span class="info-value">${task.work_type_name || '-'}</span>
|
||||
<span class="info-value">${escapeHtml(task.work_type_name || '-')}</span>
|
||||
</div>
|
||||
${task.category ? `
|
||||
<div class="info-item">
|
||||
<span class="info-label">카테고리</span>
|
||||
<span class="info-value">${task.category}</span>
|
||||
<span class="info-value">${escapeHtml(task.category)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${task.description ? `
|
||||
<div class="code-description">
|
||||
${task.description}
|
||||
${escapeHtml(task.description)}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="code-meta">
|
||||
<span>등록: ${formatDate(task.created_at)}</span>
|
||||
<span>등록: ${escapeHtml(formatDate(task.created_at))}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -275,7 +277,7 @@ function populateWorkTypeSelect() {
|
||||
|
||||
select.innerHTML = '<option value="">공정 선택...</option>' +
|
||||
workTypes.map(wt => `
|
||||
<option value="${wt.id}">${wt.name}${wt.category ? ' (' + wt.category + ')' : ''}</option>
|
||||
<option value="${escapeHtml(String(wt.id))}">${escapeHtml(wt.name)}${wt.category ? ' (' + escapeHtml(wt.category) + ')' : ''}</option>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
|
||||
@@ -493,13 +493,14 @@ function createSessionCard(session) {
|
||||
}[session.status] || '';
|
||||
|
||||
// 작업 책임자 표시 (leader_name이 있으면 표시, 없으면 created_by_name 표시)
|
||||
const leaderName = session.leader_name || session.created_by_name || '작업 책임자';
|
||||
const leaderRole = session.leader_name
|
||||
const leaderName = escapeHtml(session.leader_name || session.created_by_name || '작업 책임자');
|
||||
const leaderRole = escapeHtml(session.leader_name
|
||||
? (session.leader_job_type || '작업자')
|
||||
: '관리자';
|
||||
: '관리자');
|
||||
const safeSessionId = parseInt(session.session_id) || 0;
|
||||
|
||||
return `
|
||||
<div class="tbm-session-card" onclick="viewTbmSession(${session.session_id})">
|
||||
<div class="tbm-session-card" onclick="viewTbmSession(${safeSessionId})">
|
||||
<div class="tbm-card-header">
|
||||
<div class="tbm-card-header-top">
|
||||
<div>
|
||||
@@ -512,7 +513,7 @@ function createSessionCard(session) {
|
||||
</div>
|
||||
<div class="tbm-card-date">
|
||||
<span>📅</span>
|
||||
${formatDate(session.session_date)} ${session.start_time ? '| ' + session.start_time : ''}
|
||||
${escapeHtml(formatDate(session.session_date))} ${session.start_time ? '| ' + escapeHtml(session.start_time) : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -520,29 +521,29 @@ function createSessionCard(session) {
|
||||
<div class="tbm-card-info-grid">
|
||||
<div class="tbm-card-info-item">
|
||||
<span class="tbm-card-info-label">프로젝트</span>
|
||||
<span class="tbm-card-info-value">${session.project_name || '-'}</span>
|
||||
<span class="tbm-card-info-value">${escapeHtml(session.project_name || '-')}</span>
|
||||
</div>
|
||||
<div class="tbm-card-info-item">
|
||||
<span class="tbm-card-info-label">공정</span>
|
||||
<span class="tbm-card-info-value">${session.work_type_name || '-'}</span>
|
||||
<span class="tbm-card-info-value">${escapeHtml(session.work_type_name || '-')}</span>
|
||||
</div>
|
||||
<div class="tbm-card-info-item">
|
||||
<span class="tbm-card-info-label">작업장</span>
|
||||
<span class="tbm-card-info-value">${session.work_location || '-'}</span>
|
||||
<span class="tbm-card-info-value">${escapeHtml(session.work_location || '-')}</span>
|
||||
</div>
|
||||
<div class="tbm-card-info-item">
|
||||
<span class="tbm-card-info-label">팀원</span>
|
||||
<span class="tbm-card-info-value">${session.team_member_count || 0}명</span>
|
||||
<span class="tbm-card-info-value">${parseInt(session.team_member_count) || 0}명</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${session.status === 'draft' ? `
|
||||
<div class="tbm-card-footer">
|
||||
<button class="tbm-btn tbm-btn-primary tbm-btn-sm" onclick="event.stopPropagation(); openTeamCompositionModal(${session.session_id})">
|
||||
<button class="tbm-btn tbm-btn-primary tbm-btn-sm" onclick="event.stopPropagation(); openTeamCompositionModal(${safeSessionId})">
|
||||
👥 팀 구성
|
||||
</button>
|
||||
<button class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="event.stopPropagation(); openSafetyCheckModal(${session.session_id})">
|
||||
<button class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="event.stopPropagation(); openSafetyCheckModal(${safeSessionId})">
|
||||
✓ 안전 체크
|
||||
</button>
|
||||
</div>
|
||||
@@ -604,8 +605,8 @@ function populateLeaderSelect() {
|
||||
// 작업자와 연결된 경우: 자동으로 선택하고 비활성화
|
||||
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
|
||||
if (worker) {
|
||||
const jobTypeText = worker.job_type ? ` (${worker.job_type})` : '';
|
||||
leaderSelect.innerHTML = `<option value="${worker.worker_id}" selected>${worker.worker_name}${jobTypeText}</option>`;
|
||||
const jobTypeText = worker.job_type ? ` (${escapeHtml(worker.job_type)})` : '';
|
||||
leaderSelect.innerHTML = `<option value="${escapeHtml(worker.worker_id)}" selected>${escapeHtml(worker.worker_name)}${jobTypeText}</option>`;
|
||||
leaderSelect.disabled = true;
|
||||
console.log('✅ 입력자 자동 설정:', worker.worker_name);
|
||||
} else {
|
||||
@@ -621,8 +622,8 @@ function populateLeaderSelect() {
|
||||
|
||||
leaderSelect.innerHTML = '<option value="">입력자 선택...</option>' +
|
||||
leaders.map(w => {
|
||||
const jobTypeText = w.job_type ? ` (${w.job_type})` : '';
|
||||
return `<option value="${w.worker_id}">${w.worker_name}${jobTypeText}</option>`;
|
||||
const jobTypeText = w.job_type ? ` (${escapeHtml(w.job_type)})` : '';
|
||||
return `<option value="${escapeHtml(w.worker_id)}">${escapeHtml(w.worker_name)}${jobTypeText}</option>`;
|
||||
}).join('');
|
||||
leaderSelect.disabled = false;
|
||||
console.log('✅ 관리자: 입력자 선택 가능');
|
||||
@@ -636,7 +637,7 @@ function populateProjectSelect() {
|
||||
|
||||
projectSelect.innerHTML = '<option value="">프로젝트 선택...</option>' +
|
||||
allProjects.map(p => `
|
||||
<option value="${p.project_id}">${p.project_name} (${p.job_no})</option>
|
||||
<option value="${escapeHtml(p.project_id)}">${escapeHtml(p.project_name)} (${escapeHtml(p.job_no)})</option>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
@@ -647,7 +648,7 @@ function populateWorkTypeSelect() {
|
||||
|
||||
workTypeSelect.innerHTML = '<option value="">공정 선택...</option>' +
|
||||
allWorkTypes.map(wt => `
|
||||
<option value="${wt.id}">${wt.name}${wt.category ? ' (' + wt.category + ')' : ''}</option>
|
||||
<option value="${escapeHtml(wt.id)}">${escapeHtml(wt.name)}${wt.category ? ' (' + escapeHtml(wt.category) + ')' : ''}</option>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
@@ -658,7 +659,7 @@ function populateWorkplaceSelect() {
|
||||
|
||||
workLocationSelect.innerHTML = '<option value="">작업장 선택...</option>' +
|
||||
allWorkplaces.map(wp => `
|
||||
<option value="${wp.workplace_name}">${wp.workplace_name}${wp.location ? ' - ' + wp.location : ''}</option>
|
||||
<option value="${escapeHtml(wp.workplace_name)}">${escapeHtml(wp.workplace_name)}${wp.location ? ' - ' + escapeHtml(wp.location) : ''}</option>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
@@ -683,7 +684,7 @@ function loadTasksByWorkType() {
|
||||
taskSelect.disabled = false;
|
||||
taskSelect.innerHTML = '<option value="">작업 선택...</option>' +
|
||||
filteredTasks.map(task => `
|
||||
<option value="${task.task_id}">${task.task_name}</option>
|
||||
<option value="${escapeHtml(String(task.task_id))}">${escapeHtml(task.task_name)}</option>
|
||||
`).join('');
|
||||
|
||||
if (filteredTasks.length === 0) {
|
||||
@@ -872,12 +873,12 @@ function renderWorkerTaskList() {
|
||||
<!-- 작업자 헤더 -->
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; padding-bottom: 0.75rem; border-bottom: 1px solid #e5e7eb;">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span style="font-weight: 600; font-size: 1.1rem; color: #1f2937;">👤 ${workerData.worker_name}</span>
|
||||
<span style="font-weight: 600; font-size: 1.1rem; color: #1f2937;">👤 ${escapeHtml(workerData.worker_name)}</span>
|
||||
<span style="padding: 0.125rem 0.5rem; background: #dbeafe; color: #1e40af; border-radius: 0.25rem; font-size: 0.75rem;">
|
||||
${workerData.job_type || '작업자'}
|
||||
${escapeHtml(workerData.job_type || '작업자')}
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" onclick="removeWorkerFromList(${workerIndex})" class="btn btn-sm btn-danger">
|
||||
<button type="button" onclick="removeWorkerFromList(${parseInt(workerIndex) || 0})" class="btn btn-sm btn-danger">
|
||||
<span style="font-size: 1rem;">✕ 작업자 제거</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -887,7 +888,7 @@ function renderWorkerTaskList() {
|
||||
|
||||
<!-- 작업 추가 버튼 -->
|
||||
<div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px dashed #d1d5db;">
|
||||
<button type="button" onclick="addTaskLineToWorker(${workerIndex})" class="btn btn-sm btn-secondary" style="width: 100%;">
|
||||
<button type="button" onclick="addTaskLineToWorker(${parseInt(workerIndex) || 0})" class="btn btn-sm btn-secondary" style="width: 100%;">
|
||||
<span style="font-size: 1.2rem; margin-right: 0.25rem;">+</span>
|
||||
이 작업자의 추가 작업 등록
|
||||
</button>
|
||||
@@ -902,12 +903,14 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
|
||||
const project = allProjects.find(p => p.project_id === taskLine.project_id);
|
||||
const workType = allWorkTypes.find(wt => wt.id === taskLine.work_type_id);
|
||||
const task = allTasks.find(t => t.task_id === taskLine.task_id);
|
||||
const safeWorkerIndex = parseInt(workerIndex) || 0;
|
||||
const safeTaskIndex = parseInt(taskIndex) || 0;
|
||||
|
||||
const projectText = project ? project.project_name : '프로젝트 선택';
|
||||
const workTypeText = workType ? workType.name : '공정 선택 *';
|
||||
const taskText = task ? task.task_name : '작업 선택 *';
|
||||
const projectText = escapeHtml(project ? project.project_name : '프로젝트 선택');
|
||||
const workTypeText = escapeHtml(workType ? workType.name : '공정 선택 *');
|
||||
const taskText = escapeHtml(task ? task.task_name : '작업 선택 *');
|
||||
const workplaceText = taskLine.workplace_name
|
||||
? `${taskLine.workplace_category_name || ''} → ${taskLine.workplace_name}`
|
||||
? escapeHtml(`${taskLine.workplace_category_name || ''} → ${taskLine.workplace_name}`)
|
||||
: '작업장 선택 *';
|
||||
|
||||
return `
|
||||
@@ -915,7 +918,7 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
|
||||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<!-- 프로젝트 선택 -->
|
||||
<button type="button"
|
||||
onclick="openItemSelect('project', ${workerIndex}, ${taskIndex})"
|
||||
onclick="openItemSelect('project', ${safeWorkerIndex}, ${safeTaskIndex})"
|
||||
class="btn btn-sm ${project ? 'btn-primary' : 'btn-secondary'}"
|
||||
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;">
|
||||
📁 ${projectText}
|
||||
@@ -923,7 +926,7 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
|
||||
|
||||
<!-- 작업장 선택 -->
|
||||
<button type="button"
|
||||
onclick="openWorkplaceSelect(${workerIndex}, ${taskIndex})"
|
||||
onclick="openWorkplaceSelect(${safeWorkerIndex}, ${safeTaskIndex})"
|
||||
class="btn btn-sm ${taskLine.workplace_id ? 'btn-primary' : 'btn-secondary'}"
|
||||
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;">
|
||||
📍 ${workplaceText}
|
||||
@@ -931,7 +934,7 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
|
||||
|
||||
<!-- 공정 선택 -->
|
||||
<button type="button"
|
||||
onclick="openItemSelect('workType', ${workerIndex}, ${taskIndex})"
|
||||
onclick="openItemSelect('workType', ${safeWorkerIndex}, ${safeTaskIndex})"
|
||||
class="btn btn-sm ${workType ? 'btn-primary' : 'btn-secondary'}"
|
||||
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;">
|
||||
⚙️ ${workTypeText}
|
||||
@@ -939,7 +942,7 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
|
||||
|
||||
<!-- 작업 선택 -->
|
||||
<button type="button"
|
||||
onclick="openItemSelect('task', ${workerIndex}, ${taskIndex})"
|
||||
onclick="openItemSelect('task', ${safeWorkerIndex}, ${safeTaskIndex})"
|
||||
class="btn btn-sm ${task ? 'btn-primary' : 'btn-secondary'}"
|
||||
style="text-align: left; justify-content: flex-start; font-size: 0.875rem;"
|
||||
${!taskLine.work_type_id ? 'disabled' : ''}>
|
||||
@@ -949,7 +952,7 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
|
||||
|
||||
<!-- 작업 라인 제거 버튼 -->
|
||||
${workerData.tasks.length > 1 ? `
|
||||
<button type="button" onclick="removeTaskLine(${workerIndex}, ${taskIndex})"
|
||||
<button type="button" onclick="removeTaskLine(${safeWorkerIndex}, ${safeTaskIndex})"
|
||||
class="btn btn-sm btn-danger" style="width: 100%; font-size: 0.8rem;">
|
||||
<span style="margin-right: 0.25rem;">−</span> 이 작업 라인 제거
|
||||
</button>
|
||||
@@ -971,16 +974,17 @@ function openWorkerSelectionModal() {
|
||||
|
||||
workerCardGrid.innerHTML = allWorkers.map(worker => {
|
||||
const isAdded = addedWorkerIds.has(worker.worker_id);
|
||||
const safeWorkerId = parseInt(worker.worker_id) || 0;
|
||||
return `
|
||||
<div id="worker-card-${worker.worker_id}"
|
||||
onclick="toggleWorkerSelection(${worker.worker_id})"
|
||||
<div id="worker-card-${safeWorkerId}"
|
||||
onclick="toggleWorkerSelection(${safeWorkerId})"
|
||||
style="padding: 1rem; border: 2px solid ${isAdded ? '#d1d5db' : '#e5e7eb'}; border-radius: 0.5rem; cursor: ${isAdded ? 'not-allowed' : 'pointer'}; background: ${isAdded ? '#f3f4f6' : 'white'}; opacity: ${isAdded ? '0.5' : '1'}; transition: all 0.2s;">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
${isAdded ? '✓' : '☐'}
|
||||
<span style="font-weight: 600; font-size: 0.95rem;">${worker.worker_name}</span>
|
||||
<span style="font-weight: 600; font-size: 0.95rem;">${escapeHtml(worker.worker_name)}</span>
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: #6b7280;">
|
||||
${worker.job_type || '작업자'}${worker.department ? ' · ' + worker.department : ''}
|
||||
${escapeHtml(worker.job_type || '작업자')}${worker.department ? ' · ' + escapeHtml(worker.department) : ''}
|
||||
</div>
|
||||
${isAdded ? '<div style="font-size: 0.75rem; color: #9ca3af; margin-top: 0.25rem;">이미 추가됨</div>' : ''}
|
||||
</div>
|
||||
|
||||
@@ -83,25 +83,31 @@ function renderVacationRequests(requests, containerId, showActions = false, acti
|
||||
</thead>
|
||||
<tbody>
|
||||
${requests.map(request => {
|
||||
const statusClass = request.status === 'pending' ? 'status-pending' :
|
||||
request.status === 'approved' ? 'status-approved' : 'status-rejected';
|
||||
const statusText = request.status === 'pending' ? '대기' :
|
||||
request.status === 'approved' ? '승인' : '거부';
|
||||
const validStatuses = ['pending', 'approved', 'rejected'];
|
||||
const safeStatus = validStatuses.includes(request.status) ? request.status : 'pending';
|
||||
const statusClass = safeStatus === 'pending' ? 'status-pending' :
|
||||
safeStatus === 'approved' ? 'status-approved' : 'status-rejected';
|
||||
const statusText = safeStatus === 'pending' ? '대기' :
|
||||
safeStatus === 'approved' ? '승인' : '거부';
|
||||
const workerName = escapeHtml(request.worker_name || '알 수 없음');
|
||||
const typeName = escapeHtml(request.vacation_type_name || request.type_name || '알 수 없음');
|
||||
const reasonText = escapeHtml(request.reason || '-');
|
||||
const daysUsed = parseFloat(request.days_used) || 0;
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td><strong>${request.worker_name || '알 수 없음'}</strong></td>
|
||||
<td>${request.vacation_type_name || request.type_name || '알 수 없음'}</td>
|
||||
<td>${request.start_date}</td>
|
||||
<td>${request.end_date}</td>
|
||||
<td>${request.days_used}일</td>
|
||||
<td><strong>${workerName}</strong></td>
|
||||
<td>${typeName}</td>
|
||||
<td>${escapeHtml(request.start_date || '-')}</td>
|
||||
<td>${escapeHtml(request.end_date || '-')}</td>
|
||||
<td>${daysUsed}일</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">
|
||||
${statusText}
|
||||
</span>
|
||||
</td>
|
||||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${request.reason || '-'}">
|
||||
${request.reason || '-'}
|
||||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${reasonText}">
|
||||
${reasonText}
|
||||
</td>
|
||||
${showActions ? renderActionButtons(request, actionType) : ''}
|
||||
</tr>
|
||||
@@ -118,14 +124,15 @@ function renderVacationRequests(requests, containerId, showActions = false, acti
|
||||
* 액션 버튼 렌더링
|
||||
*/
|
||||
function renderActionButtons(request, actionType) {
|
||||
const safeRequestId = parseInt(request.request_id) || 0;
|
||||
if (actionType === 'approval' && request.status === 'pending') {
|
||||
return `
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn-small btn-success" onclick="approveVacationRequest(${request.request_id})" title="승인">
|
||||
<button class="btn-small btn-success" onclick="approveVacationRequest(${safeRequestId})" title="승인">
|
||||
✓
|
||||
</button>
|
||||
<button class="btn-small btn-danger" onclick="rejectVacationRequest(${request.request_id})" title="거부">
|
||||
<button class="btn-small btn-danger" onclick="rejectVacationRequest(${safeRequestId})" title="거부">
|
||||
✗
|
||||
</button>
|
||||
</div>
|
||||
@@ -134,7 +141,7 @@ function renderActionButtons(request, actionType) {
|
||||
} else if (actionType === 'delete' && request.status === 'pending') {
|
||||
return `
|
||||
<td>
|
||||
<button class="btn-small btn-danger" onclick="deleteVacationRequest(${request.request_id})" title="삭제">
|
||||
<button class="btn-small btn-danger" onclick="deleteVacationRequest(${safeRequestId})" title="삭제">
|
||||
삭제
|
||||
</button>
|
||||
</td>
|
||||
|
||||
@@ -69,29 +69,34 @@ function renderDepartmentList() {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = departments.map(dept => `
|
||||
<div class="department-item ${currentDepartmentId === dept.department_id ? 'active' : ''}"
|
||||
onclick="selectDepartment(${dept.department_id})">
|
||||
<div class="department-info">
|
||||
<span class="department-name">${dept.department_name}</span>
|
||||
<span class="department-count">${dept.worker_count || 0}명</span>
|
||||
container.innerHTML = departments.map(dept => {
|
||||
const safeDeptId = parseInt(dept.department_id) || 0;
|
||||
const safeDeptName = escapeHtml(dept.department_name || '-');
|
||||
const workerCount = parseInt(dept.worker_count) || 0;
|
||||
return `
|
||||
<div class="department-item ${currentDepartmentId === dept.department_id ? 'active' : ''}"
|
||||
onclick="selectDepartment(${safeDeptId})">
|
||||
<div class="department-info">
|
||||
<span class="department-name">${safeDeptName}</span>
|
||||
<span class="department-count">${workerCount}명</span>
|
||||
</div>
|
||||
<div class="department-actions" onclick="event.stopPropagation()">
|
||||
<button class="btn-icon" onclick="editDepartment(${safeDeptId})" title="수정">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon danger" onclick="confirmDeleteDepartment(${safeDeptId})" title="삭제">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="department-actions" onclick="event.stopPropagation()">
|
||||
<button class="btn-icon" onclick="editDepartment(${dept.department_id})" title="수정">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon danger" onclick="confirmDeleteDepartment(${dept.department_id})" title="삭제">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 부서 선택
|
||||
@@ -117,7 +122,10 @@ function updateParentDepartmentSelect() {
|
||||
select.innerHTML = '<option value="">없음 (최상위 부서)</option>' +
|
||||
departments
|
||||
.filter(d => d.department_id !== parseInt(currentId))
|
||||
.map(d => `<option value="${d.department_id}">${d.department_name}</option>`)
|
||||
.map(d => {
|
||||
const safeDeptId = parseInt(d.department_id) || 0;
|
||||
return `<option value="${safeDeptId}">${escapeHtml(d.department_name || '-')}</option>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
@@ -316,7 +324,8 @@ function renderWorkerList() {
|
||||
'leader': '그룹장',
|
||||
'admin': '관리자'
|
||||
};
|
||||
const jobType = jobTypeMap[worker.job_type] || worker.job_type || '-';
|
||||
const safeJobType = ['worker', 'leader', 'admin'].includes(worker.job_type) ? worker.job_type : '';
|
||||
const jobType = jobTypeMap[safeJobType] || escapeHtml(worker.job_type || '-');
|
||||
|
||||
const isInactive = worker.status === 'inactive';
|
||||
const isResigned = worker.employment_status === 'resigned';
|
||||
@@ -332,12 +341,16 @@ function renderWorkerList() {
|
||||
statusText = '사무직';
|
||||
}
|
||||
|
||||
const safeWorkerId = parseInt(worker.worker_id) || 0;
|
||||
const safeWorkerName = escapeHtml(worker.worker_name || '');
|
||||
const firstChar = safeWorkerName ? safeWorkerName.charAt(0) : '?';
|
||||
|
||||
return `
|
||||
<tr style="${isResigned ? 'opacity: 0.6;' : ''}">
|
||||
<td>
|
||||
<div class="worker-name-cell">
|
||||
<div class="worker-avatar">${worker.worker_name.charAt(0)}</div>
|
||||
<span style="font-weight: 500;">${worker.worker_name}</span>
|
||||
<div class="worker-avatar">${firstChar}</div>
|
||||
<span style="font-weight: 500;">${safeWorkerName}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>${jobType}</td>
|
||||
@@ -350,8 +363,8 @@ function renderWorkerList() {
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.25rem; justify-content: center;">
|
||||
<button class="btn-icon" onclick="editWorker(${worker.worker_id})" title="수정">✏️</button>
|
||||
<button class="btn-icon danger" onclick="confirmDeleteWorker(${worker.worker_id})" title="삭제">🗑️</button>
|
||||
<button class="btn-icon" onclick="editWorker(${safeWorkerId})" title="수정">✏️</button>
|
||||
<button class="btn-icon danger" onclick="confirmDeleteWorker(${safeWorkerId})" title="삭제">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
// 작업장 관리 페이지 JavaScript
|
||||
//
|
||||
// 참고: 이 파일은 점진적 마이그레이션 중입니다.
|
||||
// 새로운 모듈 시스템: /js/workplace-management/
|
||||
// - state.js: 전역 상태 관리
|
||||
// - utils.js: 유틸리티 함수
|
||||
// - api.js: API 클라이언트
|
||||
// - index.js: 메인 컨트롤러
|
||||
|
||||
// 전역 변수
|
||||
let categories = [];
|
||||
let workplaces = [];
|
||||
let currentCategoryId = '';
|
||||
// 전역 변수 (모듈 시스템이 없을 때만 사용)
|
||||
let categories = window.WorkplaceState?.categories || [];
|
||||
let workplaces = window.WorkplaceState?.workplaces || [];
|
||||
let currentCategoryId = window.WorkplaceState?.currentCategoryId || '';
|
||||
let currentEditingCategory = null;
|
||||
let currentEditingWorkplace = null;
|
||||
|
||||
// 페이지 초기화
|
||||
// 페이지 초기화 (모듈 시스템이 없을 때만)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('🏗️ 작업장 관리 페이지 초기화 시작');
|
||||
// 모듈 시스템이 이미 로드되어 있으면 초기화 건너뜀
|
||||
if (window.WorkplaceController) {
|
||||
console.log('[workplace-management.js] 모듈 시스템 감지 - 기존 초기화 건너뜀');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🏗️ 작업장 관리 페이지 초기화 시작 (레거시)');
|
||||
loadAllData();
|
||||
});
|
||||
|
||||
// 모든 데이터 로드
|
||||
async function loadAllData() {
|
||||
// 모듈 시스템이 있으면 위임
|
||||
if (window.WorkplaceController) {
|
||||
return window.WorkplaceController.loadAllData();
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
loadCategories(),
|
||||
@@ -35,6 +52,13 @@ async function loadAllData() {
|
||||
|
||||
// 카테고리 목록 로드
|
||||
async function loadCategories() {
|
||||
// 모듈 시스템이 있으면 위임
|
||||
if (window.WorkplaceAPI) {
|
||||
const result = await window.WorkplaceAPI.loadCategories();
|
||||
categories = window.WorkplaceState?.categories || result;
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await window.apiCall('/workplaces/categories', 'GET');
|
||||
|
||||
@@ -934,27 +958,30 @@ function showToast(message, type = 'info') {
|
||||
}
|
||||
|
||||
// 전역 함수 및 변수로 노출 (다른 모듈에서 접근 가능하도록)
|
||||
// getter/setter를 사용하여 항상 최신 값을 반환
|
||||
Object.defineProperty(window, 'categories', {
|
||||
get: function() {
|
||||
return categories;
|
||||
}
|
||||
});
|
||||
// 모듈 시스템이 이미 정의했으면 건너뜀
|
||||
if (!window.WorkplaceState) {
|
||||
// getter/setter를 사용하여 항상 최신 값을 반환
|
||||
Object.defineProperty(window, 'categories', {
|
||||
get: function() {
|
||||
return categories;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'workplaces', {
|
||||
get: function() {
|
||||
return workplaces;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(window, 'workplaces', {
|
||||
get: function() {
|
||||
return workplaces;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'currentCategoryId', {
|
||||
get: function() {
|
||||
return currentCategoryId;
|
||||
},
|
||||
set: function(value) {
|
||||
currentCategoryId = value;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(window, 'currentCategoryId', {
|
||||
get: function() {
|
||||
return currentCategoryId;
|
||||
},
|
||||
set: function(value) {
|
||||
currentCategoryId = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 작업장 지도 관리 ====================
|
||||
|
||||
|
||||
@@ -442,15 +442,15 @@ function renderCurrentTasks(workers) {
|
||||
html += `
|
||||
<div class="current-task-item">
|
||||
<div class="task-info">
|
||||
<p class="task-name">${worker.task_name}</p>
|
||||
<p class="task-name">${escapeHtml(worker.task_name)}</p>
|
||||
<p class="task-detail">
|
||||
${worker.work_location ? `📍 ${worker.work_location}` : ''}
|
||||
${worker.project_name ? ` • 📁 ${worker.project_name}` : ''}
|
||||
${worker.work_location ? `📍 ${escapeHtml(worker.work_location)}` : ''}
|
||||
${worker.project_name ? ` • 📁 ${escapeHtml(worker.project_name)}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div class="task-badge">
|
||||
<span>👷</span>
|
||||
<span>${worker.member_count}명</span>
|
||||
<span>${parseInt(worker.member_count) || 0}명</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -481,7 +481,7 @@ async function loadEquipmentStatus(workplaceId) {
|
||||
<div class="equipment-item">
|
||||
<span class="equipment-icon">⚙️</span>
|
||||
<div class="equipment-info">
|
||||
<p class="equipment-name">${eq.equipment_name}</p>
|
||||
<p class="equipment-name">${escapeHtml(eq.equipment_name)}</p>
|
||||
<p class="equipment-status ${statusClass}">${statusText}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -516,11 +516,11 @@ function renderWorkersTab(workers) {
|
||||
html += `
|
||||
<div class="worker-item">
|
||||
<div class="worker-item-header">
|
||||
<p class="worker-item-title">${worker.task_name}</p>
|
||||
<span class="worker-item-badge">${worker.member_count}명</span>
|
||||
<p class="worker-item-title">${escapeHtml(worker.task_name)}</p>
|
||||
<span class="worker-item-badge">${parseInt(worker.member_count) || 0}명</span>
|
||||
</div>
|
||||
${worker.work_location ? `<p class="worker-item-detail">📍 ${worker.work_location}</p>` : ''}
|
||||
${worker.project_name ? `<p class="worker-item-detail">📁 ${worker.project_name}</p>` : ''}
|
||||
${worker.work_location ? `<p class="worker-item-detail">📍 ${escapeHtml(worker.work_location)}</p>` : ''}
|
||||
${worker.project_name ? `<p class="worker-item-detail">📁 ${escapeHtml(worker.project_name)}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
@@ -544,11 +544,11 @@ function renderVisitorsTab(visitors) {
|
||||
html += `
|
||||
<div class="visitor-item">
|
||||
<div class="visitor-item-header">
|
||||
<p class="visitor-item-title">${visitor.visitor_company}</p>
|
||||
<span class="visitor-item-badge">${visitor.visitor_count}명 • ${statusText}</span>
|
||||
<p class="visitor-item-title">${escapeHtml(visitor.visitor_company)}</p>
|
||||
<span class="visitor-item-badge">${parseInt(visitor.visitor_count) || 0}명 • ${statusText}</span>
|
||||
</div>
|
||||
<p class="visitor-item-detail">⏰ ${visitor.visit_time}</p>
|
||||
<p class="visitor-item-detail">📋 ${visitor.purpose_name}</p>
|
||||
<p class="visitor-item-detail">⏰ ${escapeHtml(visitor.visit_time)}</p>
|
||||
<p class="visitor-item-detail">📋 ${escapeHtml(visitor.purpose_name)}</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user