feat: 페이지 구조 재구성 및 사이드바 네비게이션 구현
- 페이지 폴더 재구성: safety/, attendance/ 폴더 신규 생성 - work/ → safety/: 이슈 신고, 출입 신청 관련 페이지 이동 - common/ → attendance/: 근태/휴가 관련 페이지 이동 - admin/ 정리: safety-* 파일들을 safety/로 이동 - 사이드바 네비게이션 메뉴 구현 - 카테고리별 메뉴: 작업관리, 안전관리, 근태관리, 시스템관리 - 접기/펼치기 기능 및 상태 저장 - 관리자 전용 메뉴 자동 표시/숨김 - 날씨 API 연동 (기상청 단기예보) - TBM 및 navbar에 현재 날씨 표시 - weatherService.js 추가 - 안전 체크리스트 확장 - 기본/날씨별/작업별 체크 유형 추가 - checklist-manage.html 페이지 추가 - 이슈 신고 시스템 구현 - workIssueController, workIssueModel, workIssueRoutes 추가 - DB 마이그레이션 파일 추가 (실행 대기) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
455
web-ui/js/tbm.js
455
web-ui/js/tbm.js
@@ -26,6 +26,11 @@ let selectedWorkplaceName = '';
|
||||
let isBulkMode = false; // 일괄 설정 모드인지 여부
|
||||
let bulkSelectedWorkers = new Set(); // 일괄 설정에서 선택된 작업자 인덱스
|
||||
|
||||
// TBM 관리 탭용 변수
|
||||
let loadedDaysCount = 7; // 처음에 로드할 일수
|
||||
let dateGroupedSessions = {}; // 날짜별로 그룹화된 세션
|
||||
let allLoadedSessions = []; // 전체 로드된 세션
|
||||
|
||||
// ==================== 유틸리티 함수 ====================
|
||||
|
||||
/**
|
||||
@@ -87,8 +92,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// 오늘 날짜 설정 (서울 시간대 기준)
|
||||
const today = getTodayKST();
|
||||
document.getElementById('tbmDate').value = today;
|
||||
document.getElementById('sessionDate').value = today;
|
||||
const tbmDateEl = document.getElementById('tbmDate');
|
||||
const sessionDateEl = document.getElementById('sessionDate');
|
||||
if (tbmDateEl) tbmDateEl.value = today;
|
||||
if (sessionDateEl) sessionDateEl.value = today;
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
setupEventListeners();
|
||||
@@ -100,22 +107,16 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
function setupEventListeners() {
|
||||
const tbmDateInput = document.getElementById('tbmDate');
|
||||
if (tbmDateInput) {
|
||||
tbmDateInput.addEventListener('change', () => {
|
||||
const date = tbmDateInput.value;
|
||||
loadTbmSessionsByDate(date);
|
||||
});
|
||||
}
|
||||
// 날짜 선택기 제거됨 - 날짜별 그룹 뷰 사용
|
||||
}
|
||||
|
||||
// 초기 데이터 로드
|
||||
async function loadInitialData() {
|
||||
try {
|
||||
// 현재 로그인한 사용자 정보 가져오기
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
const userInfo = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
currentUser = userInfo;
|
||||
console.log('👤 로그인 사용자:', currentUser);
|
||||
console.log('👤 로그인 사용자:', currentUser, 'worker_id:', currentUser?.worker_id);
|
||||
|
||||
// 작업자 목록 로드
|
||||
const workersResponse = await window.apiCall('/workers?limit=1000');
|
||||
@@ -202,12 +203,7 @@ function switchTbmTab(tabName) {
|
||||
if (tabName === 'tbm-input') {
|
||||
loadTodayOnlyTbm();
|
||||
} else if (tabName === 'tbm-manage') {
|
||||
const tbmDate = document.getElementById('tbmDate');
|
||||
if (tbmDate && tbmDate.value) {
|
||||
loadTbmSessionsByDate(tbmDate.value);
|
||||
} else {
|
||||
loadTodayTbm();
|
||||
}
|
||||
loadRecentTbmGroupedByDate();
|
||||
}
|
||||
}
|
||||
window.switchTbmTab = switchTbmTab;
|
||||
@@ -268,36 +264,175 @@ function displayTodayTbmSessions() {
|
||||
|
||||
// ==================== TBM 관리 탭 ====================
|
||||
|
||||
// 오늘 TBM 로드 (TBM 관리 탭용)
|
||||
// 오늘 TBM 로드 (TBM 관리 탭용) - 레거시 호환
|
||||
async function loadTodayTbm() {
|
||||
const today = getTodayKST();
|
||||
document.getElementById('tbmDate').value = today;
|
||||
await loadTbmSessionsByDate(today);
|
||||
await loadRecentTbmGroupedByDate();
|
||||
}
|
||||
window.loadTodayTbm = loadTodayTbm;
|
||||
|
||||
// 전체 TBM 로드
|
||||
// 전체 TBM 로드 - 레거시 호환
|
||||
async function loadAllTbm() {
|
||||
try {
|
||||
const response = await window.apiCall('/tbm/sessions');
|
||||
|
||||
if (response && response.success) {
|
||||
allSessions = response.data || [];
|
||||
document.getElementById('tbmDate').value = '';
|
||||
displayTbmSessions();
|
||||
} else {
|
||||
allSessions = [];
|
||||
displayTbmSessions();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 전체 TBM 조회 오류:', error);
|
||||
showToast('전체 TBM을 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
allSessions = [];
|
||||
displayTbmSessions();
|
||||
}
|
||||
loadedDaysCount = 30; // 30일치 로드
|
||||
await loadRecentTbmGroupedByDate();
|
||||
}
|
||||
window.loadAllTbm = loadAllTbm;
|
||||
|
||||
// ==================== 날짜별 그룹 TBM 로드 (새 기능) ====================
|
||||
|
||||
/**
|
||||
* 사용자가 Admin인지 확인
|
||||
*/
|
||||
function isAdminUser() {
|
||||
if (!currentUser) return false;
|
||||
return currentUser.role === 'Admin' || currentUser.role === 'System Admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* 최근 TBM을 날짜별로 그룹화하여 로드
|
||||
*/
|
||||
async function loadRecentTbmGroupedByDate() {
|
||||
try {
|
||||
const today = new Date();
|
||||
const dates = [];
|
||||
|
||||
// 최근 N일의 날짜 생성
|
||||
for (let i = 0; i < loadedDaysCount; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
dates.push(dateStr);
|
||||
}
|
||||
|
||||
// 각 날짜의 TBM 로드
|
||||
dateGroupedSessions = {};
|
||||
allLoadedSessions = [];
|
||||
|
||||
const promises = dates.map(date => window.apiCall(`/tbm/sessions/date/${date}`));
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
results.forEach((response, index) => {
|
||||
const date = dates[index];
|
||||
if (response && response.success && response.data && response.data.length > 0) {
|
||||
let sessions = response.data;
|
||||
|
||||
// admin이 아니면 본인이 작성한 TBM만 필터링
|
||||
if (!isAdminUser()) {
|
||||
const userId = currentUser?.user_id;
|
||||
const workerId = currentUser?.worker_id;
|
||||
sessions = sessions.filter(s => {
|
||||
return s.created_by === userId ||
|
||||
s.leader_id === workerId ||
|
||||
s.created_by_name === currentUser?.name;
|
||||
});
|
||||
}
|
||||
|
||||
if (sessions.length > 0) {
|
||||
dateGroupedSessions[date] = sessions;
|
||||
allLoadedSessions = allLoadedSessions.concat(sessions);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 날짜별 그룹 표시
|
||||
displayTbmGroupedByDate();
|
||||
|
||||
// 뷰 모드 표시
|
||||
updateViewModeIndicator();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ TBM 날짜별 로드 오류:', error);
|
||||
showToast('TBM을 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
dateGroupedSessions = {};
|
||||
displayTbmGroupedByDate();
|
||||
}
|
||||
}
|
||||
window.loadRecentTbmGroupedByDate = loadRecentTbmGroupedByDate;
|
||||
|
||||
/**
|
||||
* 뷰 모드 표시 업데이트
|
||||
*/
|
||||
function updateViewModeIndicator() {
|
||||
const indicator = document.getElementById('viewModeIndicator');
|
||||
const text = document.getElementById('viewModeText');
|
||||
|
||||
if (indicator && text) {
|
||||
if (isAdminUser()) {
|
||||
indicator.style.display = 'none'; // Admin은 표시 안 함 (전체가 기본)
|
||||
} else {
|
||||
indicator.style.display = 'inline-flex';
|
||||
text.textContent = '내 TBM';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜별 그룹으로 TBM 표시
|
||||
*/
|
||||
function displayTbmGroupedByDate() {
|
||||
const container = document.getElementById('tbmDateGroupsContainer');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
const totalSessionsEl = document.getElementById('totalSessions');
|
||||
const completedSessionsEl = document.getElementById('completedSessions');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
// 날짜별로 정렬 (최신순)
|
||||
const sortedDates = Object.keys(dateGroupedSessions).sort((a, b) => new Date(b) - new Date(a));
|
||||
|
||||
if (sortedDates.length === 0 || allLoadedSessions.length === 0) {
|
||||
container.innerHTML = '';
|
||||
if (emptyState) emptyState.style.display = 'flex';
|
||||
if (totalSessionsEl) totalSessionsEl.textContent = '0';
|
||||
if (completedSessionsEl) completedSessionsEl.textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyState) emptyState.style.display = 'none';
|
||||
|
||||
// 통계 업데이트
|
||||
const completedCount = allLoadedSessions.filter(s => s.status === 'completed').length;
|
||||
if (totalSessionsEl) totalSessionsEl.textContent = allLoadedSessions.length;
|
||||
if (completedSessionsEl) completedSessionsEl.textContent = completedCount;
|
||||
|
||||
// 날짜별 그룹 HTML 생성
|
||||
const today = getTodayKST();
|
||||
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
|
||||
container.innerHTML = sortedDates.map(date => {
|
||||
const sessions = dateGroupedSessions[date];
|
||||
const dateObj = new Date(date + 'T00:00:00');
|
||||
const dayName = dayNames[dateObj.getDay()];
|
||||
const isToday = date === today;
|
||||
|
||||
// 날짜 포맷팅 (YYYY-MM-DD → MM월 DD일)
|
||||
const [year, month, day] = date.split('-');
|
||||
const displayDate = `${parseInt(month)}월 ${parseInt(day)}일`;
|
||||
|
||||
return `
|
||||
<div class="date-group">
|
||||
<div class="date-group-header ${isToday ? 'today' : ''}">
|
||||
<span class="date-group-date">${displayDate}</span>
|
||||
<span class="date-group-day">${dayName}요일${isToday ? ' (오늘)' : ''}</span>
|
||||
<span class="date-group-count">${sessions.length}건</span>
|
||||
</div>
|
||||
<div class="date-group-grid">
|
||||
${sessions.map(session => createSessionCard(session)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 더 많은 날짜 로드
|
||||
*/
|
||||
async function loadMoreTbmDays() {
|
||||
loadedDaysCount += 7; // 7일씩 추가
|
||||
await loadRecentTbmGroupedByDate();
|
||||
showToast(`최근 ${loadedDaysCount}일의 TBM을 로드했습니다.`, 'success');
|
||||
}
|
||||
window.loadMoreTbmDays = loadMoreTbmDays;
|
||||
|
||||
// 특정 날짜의 TBM 세션 목록 로드
|
||||
async function loadTbmSessionsByDate(date) {
|
||||
try {
|
||||
@@ -318,28 +453,22 @@ async function loadTbmSessionsByDate(date) {
|
||||
}
|
||||
}
|
||||
|
||||
// TBM 세션 목록 표시 (관리 탭용)
|
||||
// TBM 세션 목록 표시 (관리 탭용) - 레거시 호환 (날짜별 그룹 뷰 사용)
|
||||
function displayTbmSessions() {
|
||||
const grid = document.getElementById('tbmSessionsGrid');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
const totalSessionsEl = document.getElementById('totalSessions');
|
||||
const completedSessionsEl = document.getElementById('completedSessions');
|
||||
|
||||
if (allSessions.length === 0) {
|
||||
grid.innerHTML = '';
|
||||
emptyState.style.display = 'flex';
|
||||
totalSessionsEl.textContent = '0';
|
||||
completedSessionsEl.textContent = '0';
|
||||
return;
|
||||
// 새 날짜별 그룹 뷰로 리다이렉트
|
||||
if (allSessions.length > 0) {
|
||||
// allSessions를 날짜별로 그룹화
|
||||
dateGroupedSessions = {};
|
||||
allSessions.forEach(session => {
|
||||
const date = formatDate(session.session_date);
|
||||
if (!dateGroupedSessions[date]) {
|
||||
dateGroupedSessions[date] = [];
|
||||
}
|
||||
dateGroupedSessions[date].push(session);
|
||||
});
|
||||
allLoadedSessions = allSessions;
|
||||
}
|
||||
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
const completedCount = allSessions.filter(s => s.status === 'completed').length;
|
||||
totalSessionsEl.textContent = allSessions.length;
|
||||
completedSessionsEl.textContent = completedCount;
|
||||
|
||||
grid.innerHTML = allSessions.map(session => createSessionCard(session)).join('');
|
||||
displayTbmGroupedByDate();
|
||||
}
|
||||
|
||||
// TBM 세션 카드 생성 (공통)
|
||||
@@ -432,12 +561,12 @@ function openNewTbmModal() {
|
||||
if (currentUser && currentUser.worker_id) {
|
||||
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
|
||||
if (worker) {
|
||||
document.getElementById('leaderName').value = `${worker.worker_name} (${worker.job_type || ''})`;
|
||||
document.getElementById('leaderName').value = worker.worker_name;
|
||||
document.getElementById('leaderId').value = worker.worker_id;
|
||||
}
|
||||
} else if (currentUser && currentUser.name) {
|
||||
// 관리자: 관리자로 표시
|
||||
document.getElementById('leaderName').value = `${currentUser.name} (관리자)`;
|
||||
// 관리자: 이름만 표시
|
||||
document.getElementById('leaderName').value = currentUser.name;
|
||||
document.getElementById('leaderId').value = '';
|
||||
}
|
||||
|
||||
@@ -459,7 +588,8 @@ function populateLeaderSelect() {
|
||||
// 작업자와 연결된 경우: 자동으로 선택하고 비활성화
|
||||
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
|
||||
if (worker) {
|
||||
leaderSelect.innerHTML = `<option value="${worker.worker_id}" selected>${worker.worker_name} (${worker.job_type || ''})</option>`;
|
||||
const jobTypeText = worker.job_type ? ` (${worker.job_type})` : '';
|
||||
leaderSelect.innerHTML = `<option value="${worker.worker_id}" selected>${worker.worker_name}${jobTypeText}</option>`;
|
||||
leaderSelect.disabled = true;
|
||||
console.log('✅ 입력자 자동 설정:', worker.worker_name);
|
||||
} else {
|
||||
@@ -474,9 +604,10 @@ function populateLeaderSelect() {
|
||||
);
|
||||
|
||||
leaderSelect.innerHTML = '<option value="">입력자 선택...</option>' +
|
||||
leaders.map(w => `
|
||||
<option value="${w.worker_id}">${w.worker_name} (${w.job_type || ''})</option>
|
||||
`).join('');
|
||||
leaders.map(w => {
|
||||
const jobTypeText = w.job_type ? ` (${w.job_type})` : '';
|
||||
return `<option value="${w.worker_id}">${w.worker_name}${jobTypeText}</option>`;
|
||||
}).join('');
|
||||
leaderSelect.disabled = false;
|
||||
console.log('✅ 관리자: 입력자 선택 가능');
|
||||
}
|
||||
@@ -1856,65 +1987,92 @@ async function saveTeamComposition() {
|
||||
}
|
||||
window.saveTeamComposition = saveTeamComposition;
|
||||
|
||||
// 안전 체크 모달 열기
|
||||
// 안전 체크 모달 열기 (기본 + 날씨별 + 작업별)
|
||||
async function openSafetyCheckModal(sessionId) {
|
||||
currentSessionId = sessionId;
|
||||
|
||||
// 기존 안전 체크 기록 로드
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/safety`);
|
||||
const existingRecords = response && response.success ? response.data : [];
|
||||
// 필터링된 체크리스트 조회 (기본 + 날씨 + 작업별)
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/safety-checks/filtered`);
|
||||
|
||||
// 카테고리별로 그룹화
|
||||
const grouped = {};
|
||||
allSafetyChecks.forEach(check => {
|
||||
if (!grouped[check.check_category]) {
|
||||
grouped[check.check_category] = [];
|
||||
}
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '체크리스트를 불러올 수 없습니다.');
|
||||
}
|
||||
|
||||
const existingRecord = existingRecords.find(r => r.check_id === check.check_id);
|
||||
grouped[check.check_category].push({
|
||||
...check,
|
||||
is_checked: existingRecord ? existingRecord.is_checked : false,
|
||||
notes: existingRecord ? existingRecord.notes : ''
|
||||
});
|
||||
});
|
||||
const { basic, weather, task, weatherInfo } = response.data;
|
||||
|
||||
const categoryNames = {
|
||||
'PPE': '개인 보호 장비',
|
||||
'EQUIPMENT': '장비 점검',
|
||||
'ENVIRONMENT': '작업 환경',
|
||||
'EMERGENCY': '비상 대응'
|
||||
'EMERGENCY': '비상 대응',
|
||||
'WEATHER': '날씨',
|
||||
'TASK': '작업'
|
||||
};
|
||||
|
||||
const weatherIcons = {
|
||||
clear: '☀️', rain: '🌧️', snow: '❄️', heat: '🔥',
|
||||
cold: '🥶', wind: '💨', fog: '🌫️', dust: '😷'
|
||||
};
|
||||
|
||||
const container = document.getElementById('safetyChecklistContainer');
|
||||
container.innerHTML = Object.keys(grouped).map(category => `
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<div style="font-weight: 600; font-size: 0.9375rem; color: #374151; padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem; margin-bottom: 0.5rem;">
|
||||
${categoryNames[category] || category}
|
||||
</div>
|
||||
${grouped[category].map(check => `
|
||||
<div style="padding: 0.75rem; border-bottom: 1px solid #f3f4f6;">
|
||||
<label style="display: flex; align-items: start; gap: 0.75rem; cursor: pointer;">
|
||||
<input type="checkbox"
|
||||
class="safety-check"
|
||||
data-check-id="${check.check_id}"
|
||||
${check.is_checked ? 'checked' : ''}
|
||||
${check.is_required ? 'required' : ''}
|
||||
style="width: 18px; height: 18px; margin-top: 0.125rem; cursor: pointer;">
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 500; color: #111827;">
|
||||
${check.check_item}
|
||||
${check.is_required ? '<span style="color: #ef4444;">*</span>' : ''}
|
||||
</div>
|
||||
${check.description ? `<div style="font-size: 0.75rem; color: #6b7280; margin-top: 0.25rem;">${check.description}</div>` : ''}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('');
|
||||
let html = '';
|
||||
|
||||
// 1. 기본 사항 섹션
|
||||
if (basic && basic.length > 0) {
|
||||
const basicGrouped = groupChecksByCategory(basic);
|
||||
html += `
|
||||
<div class="safety-section" style="margin-bottom: 1.5rem;">
|
||||
<div style="font-weight: 700; font-size: 1rem; color: #1f2937; padding: 0.75rem 1rem; background: linear-gradient(135deg, #3b82f6, #2563eb); color: white; border-radius: 0.5rem; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>📋</span> 기본 안전 사항 (${basic.length}개)
|
||||
</div>
|
||||
${renderCategoryGroups(basicGrouped, categoryNames)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 2. 날씨별 섹션
|
||||
if (weather && weather.length > 0) {
|
||||
const weatherConditions = weatherInfo?.weather_conditions || [];
|
||||
const conditionNames = weatherConditions.map(c => {
|
||||
const icon = weatherIcons[c] || '🌤️';
|
||||
return `${icon} ${getWeatherConditionName(c)}`;
|
||||
}).join(', ') || '맑음';
|
||||
|
||||
html += `
|
||||
<div class="safety-section" style="margin-bottom: 1.5rem;">
|
||||
<div style="font-weight: 700; font-size: 1rem; color: #1f2937; padding: 0.75rem 1rem; background: linear-gradient(135deg, #f59e0b, #d97706); color: white; border-radius: 0.5rem; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>🌤️</span> 오늘 날씨 관련 (${conditionNames}) - ${weather.length}개
|
||||
</div>
|
||||
${renderCheckItems(weather)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 3. 작업별 섹션
|
||||
if (task && task.length > 0) {
|
||||
const taskGrouped = groupChecksByTask(task);
|
||||
html += `
|
||||
<div class="safety-section" style="margin-bottom: 1.5rem;">
|
||||
<div style="font-weight: 700; font-size: 1rem; color: #1f2937; padding: 0.75rem 1rem; background: linear-gradient(135deg, #10b981, #059669); color: white; border-radius: 0.5rem; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>🔧</span> 작업별 안전 사항 - ${task.length}개
|
||||
</div>
|
||||
${renderTaskGroups(taskGrouped)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 체크리스트가 없는 경우
|
||||
if ((!basic || basic.length === 0) && (!weather || weather.length === 0) && (!task || task.length === 0)) {
|
||||
html = `
|
||||
<div style="text-align: center; padding: 2rem; color: #6b7280;">
|
||||
<div style="font-size: 2rem; margin-bottom: 0.5rem;">📋</div>
|
||||
<p>등록된 안전 체크 항목이 없습니다.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
document.getElementById('safetyModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
@@ -1925,6 +2083,83 @@ async function openSafetyCheckModal(sessionId) {
|
||||
}
|
||||
window.openSafetyCheckModal = openSafetyCheckModal;
|
||||
|
||||
// 카테고리별 그룹화
|
||||
function groupChecksByCategory(checks) {
|
||||
return checks.reduce((acc, check) => {
|
||||
const category = check.check_category || 'OTHER';
|
||||
if (!acc[category]) acc[category] = [];
|
||||
acc[category].push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// 작업별 그룹화
|
||||
function groupChecksByTask(checks) {
|
||||
return checks.reduce((acc, check) => {
|
||||
const taskId = check.task_id || 0;
|
||||
const taskName = check.task_name || '기타 작업';
|
||||
if (!acc[taskId]) acc[taskId] = { name: taskName, items: [] };
|
||||
acc[taskId].items.push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// 날씨 조건명 반환
|
||||
function getWeatherConditionName(code) {
|
||||
const names = {
|
||||
clear: '맑음', rain: '비', snow: '눈', heat: '폭염',
|
||||
cold: '한파', wind: '강풍', fog: '안개', dust: '미세먼지'
|
||||
};
|
||||
return names[code] || code;
|
||||
}
|
||||
|
||||
// 카테고리 그룹 렌더링
|
||||
function renderCategoryGroups(grouped, categoryNames) {
|
||||
return Object.keys(grouped).map(category => `
|
||||
<div style="margin-bottom: 1rem; background: white; border-radius: 0.5rem; border: 1px solid #e5e7eb; overflow: hidden;">
|
||||
<div style="font-weight: 600; font-size: 0.875rem; color: #374151; padding: 0.625rem 0.875rem; background: #f9fafb; border-bottom: 1px solid #e5e7eb;">
|
||||
${categoryNames[category] || category}
|
||||
</div>
|
||||
${renderCheckItems(grouped[category])}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 작업 그룹 렌더링
|
||||
function renderTaskGroups(grouped) {
|
||||
return Object.values(grouped).map(group => `
|
||||
<div style="margin-bottom: 1rem; background: white; border-radius: 0.5rem; border: 1px solid #e5e7eb; overflow: hidden;">
|
||||
<div style="font-weight: 600; font-size: 0.875rem; color: #374151; padding: 0.625rem 0.875rem; background: #f9fafb; border-bottom: 1px solid #e5e7eb;">
|
||||
📋 ${group.name}
|
||||
</div>
|
||||
${renderCheckItems(group.items)}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 체크 항목 렌더링
|
||||
function renderCheckItems(items) {
|
||||
return items.map(check => `
|
||||
<div style="padding: 0.75rem; border-bottom: 1px solid #f3f4f6;">
|
||||
<label style="display: flex; align-items: start; gap: 0.75rem; cursor: pointer;">
|
||||
<input type="checkbox"
|
||||
class="safety-check"
|
||||
data-check-id="${check.check_id}"
|
||||
${check.is_checked ? 'checked' : ''}
|
||||
${check.is_required ? 'required' : ''}
|
||||
style="width: 18px; height: 18px; margin-top: 0.125rem; cursor: pointer;">
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 500; color: #111827;">
|
||||
${check.check_item}
|
||||
${check.is_required ? '<span style="color: #ef4444;">*</span>' : ''}
|
||||
</div>
|
||||
${check.description ? `<div style="font-size: 0.75rem; color: #6b7280; margin-top: 0.25rem;">${check.description}</div>` : ''}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 안전 체크 모달 닫기
|
||||
function closeSafetyModal() {
|
||||
document.getElementById('safetyModal').style.display = 'none';
|
||||
|
||||
Reference in New Issue
Block a user