refactor: TBM/작업보고 코드 통합 및 API 쿼리 버그 수정
- 공통 유틸리티 추출 (common/utils.js, common/base-state.js) - TBM 모바일 인라인 JS/CSS 외부 파일로 분리 (tbm-mobile.js, tbm-mobile.css) - 미사용 코드 삭제 (index.js, work-report-*.js 등 5개 파일) - TBM/작업보고 state.js, utils.js를 공통 모듈 기반으로 전환 - 작업보고서 SSO 인증 호환 수정 (token/user 함수) - tbmModel.js: incomplete-reports 쿼리에서 users→sso_users 조인 수정, leader_name 조인 추가 - docker-compose.yml: system1-web 볼륨 마운트 추가 - 모바일 인계(handover) 기능 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
export function parseJwt(token) {
|
||||
try {
|
||||
// 토큰의 두 번째 부분(payload)을 base64 디코딩하고 JSON으로 파싱
|
||||
return JSON.parse(atob(token.split('.')[1]));
|
||||
} catch (e) {
|
||||
console.error("잘못된 토큰입니다.", e);
|
||||
@@ -20,7 +19,7 @@ export function parseJwt(token) {
|
||||
*/
|
||||
export function getToken() {
|
||||
if (window.getSSOToken) return window.getSSOToken();
|
||||
return localStorage.getItem('sso_token');
|
||||
return localStorage.getItem('sso_token') || localStorage.getItem('token');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,9 +27,9 @@ export function getToken() {
|
||||
*/
|
||||
export function getUser() {
|
||||
if (window.getSSOUser) return window.getSSOUser();
|
||||
const user = localStorage.getItem('sso_user');
|
||||
const raw = localStorage.getItem('sso_user') || localStorage.getItem('user');
|
||||
try {
|
||||
return user ? JSON.parse(user) : null;
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
@@ -38,10 +37,16 @@ export function getUser() {
|
||||
|
||||
/**
|
||||
* 로그인 성공 후 토큰과 사용자 정보를 저장합니다.
|
||||
* 하위 호환성을 위해 sso_token/sso_user와 token/user 모두에 저장합니다.
|
||||
*/
|
||||
export function saveAuthData(token, user) {
|
||||
const userStr = JSON.stringify(user);
|
||||
// SSO 키
|
||||
localStorage.setItem('sso_token', token);
|
||||
localStorage.setItem('sso_user', JSON.stringify(user));
|
||||
localStorage.setItem('sso_user', userStr);
|
||||
// 하위 호환 키 (캐시된 구버전 app-init.js 대응)
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('user', userStr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +56,9 @@ export function clearAuthData() {
|
||||
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
|
||||
localStorage.removeItem('sso_token');
|
||||
localStorage.removeItem('sso_user');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('userPageAccess');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,4 +67,4 @@ export function clearAuthData() {
|
||||
export function isLoggedIn() {
|
||||
const token = getToken();
|
||||
return !!token && token !== 'undefined' && token !== 'null';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const config = {
|
||||
// 페이지 경로 설정
|
||||
paths: {
|
||||
// 로그인 페이지 경로
|
||||
loginPage: '/login',
|
||||
loginPage: '/index.html',
|
||||
// 메인 대시보드 경로 (모든 사용자 공통)
|
||||
dashboard: '/pages/dashboard.html',
|
||||
// 하위 호환성을 위한 별칭들
|
||||
@@ -39,4 +39,4 @@ export const config = {
|
||||
// 토큰 만료 확인 주기 (밀리초 단위, 예: 5분)
|
||||
tokenRefreshInterval: 5 * 60 * 1000,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -70,17 +70,10 @@ const MobileReport = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 유틸리티 =====
|
||||
function getKoreaToday() {
|
||||
const today = new Date();
|
||||
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function formatDateForApi(date) {
|
||||
if (!date) return null;
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
// ===== 유틸리티 (CommonUtils 위임) =====
|
||||
var CU = window.CommonUtils;
|
||||
function getKoreaToday() { return CU.getTodayKST(); }
|
||||
function formatDateForApi(date) { return CU.formatDate(date) || null; }
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
@@ -88,10 +81,7 @@ const MobileReport = (function() {
|
||||
return `${d.getMonth() + 1}/${d.getDate()}`;
|
||||
}
|
||||
|
||||
function getDayOfWeek(dateString) {
|
||||
const days = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
return days[new Date(dateString).getDay()];
|
||||
}
|
||||
function getDayOfWeek(dateString) { return CU.getDayOfWeek(dateString); }
|
||||
|
||||
function formatHours(val) {
|
||||
if (!val || val <= 0) return '선택';
|
||||
|
||||
@@ -5,41 +5,24 @@
|
||||
// =================================================================
|
||||
// API 설정은 api-config.js에서 window 객체에 설정됨
|
||||
|
||||
// 전역 변수
|
||||
let workTypes = [];
|
||||
let workStatusTypes = [];
|
||||
let errorTypes = []; // 레거시 호환용
|
||||
let issueCategories = []; // 신고 카테고리 (nonconformity)
|
||||
let issueItems = []; // 신고 아이템
|
||||
let workers = [];
|
||||
let projects = [];
|
||||
let selectedWorkers = new Set();
|
||||
let workEntryCounter = 0;
|
||||
let currentStep = 1;
|
||||
let editingWorkId = null; // 수정 중인 작업 ID
|
||||
let incompleteTbms = []; // 미완료 TBM 작업 목록
|
||||
let currentTab = 'tbm'; // 현재 활성 탭
|
||||
// 전역 변수 → DailyWorkReportState 프록시 사용 (state.js에서 window 프록시 정의)
|
||||
// workTypes, workStatusTypes, errorTypes, issueCategories, issueItems,
|
||||
// workers, projects, selectedWorkers, incompleteTbms, tempDefects,
|
||||
// dailyIssuesCache, currentTab, currentStep, editingWorkId, workEntryCounter,
|
||||
// currentDefectIndex, currentEditingField, currentTimeValue,
|
||||
// selectedWorkplace, selectedWorkplaceName, selectedWorkplaceCategory, selectedWorkplaceCategoryName
|
||||
|
||||
// 부적합 원인 관리
|
||||
let currentDefectIndex = null; // 현재 편집 중인 행 인덱스
|
||||
let tempDefects = {}; // 임시 부적합 원인 저장 { index: [{ error_type_id, defect_hours, note }] }
|
||||
|
||||
// 작업장소 지도 관련 변수
|
||||
let mapCanvas = null;
|
||||
let mapCtx = null;
|
||||
let mapImage = null;
|
||||
let mapRegions = [];
|
||||
let selectedWorkplace = null;
|
||||
let selectedWorkplaceName = null;
|
||||
let selectedWorkplaceCategory = null;
|
||||
let selectedWorkplaceCategoryName = null;
|
||||
// 지도 관련 변수 (프록시 아님)
|
||||
var mapCanvas = null;
|
||||
var mapCtx = null;
|
||||
var mapImage = null;
|
||||
var mapRegions = [];
|
||||
|
||||
// 시간 선택 관련 변수
|
||||
let currentEditingField = null; // { index, type: 'total' | 'error' }
|
||||
let currentTimeValue = 0;
|
||||
// currentEditingField, currentTimeValue → DailyWorkReportState 프록시 사용
|
||||
|
||||
// 당일 신고 리마인더 관련 변수
|
||||
let dailyIssuesCache = {}; // { 'YYYY-MM-DD': [issues] } - 날짜별 신고 캐시
|
||||
// dailyIssuesCache → DailyWorkReportState 프록시 사용
|
||||
|
||||
// =================================================================
|
||||
// TBM 작업보고 관련 함수
|
||||
@@ -182,75 +165,23 @@ function getRelatedIssues(dateStr, workplaceId, projectId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜를 API 형식(YYYY-MM-DD)으로 변환 - 로컬 시간대 기준
|
||||
* 날짜를 API 형식(YYYY-MM-DD)으로 변환
|
||||
*/
|
||||
function formatDateForApi(date) {
|
||||
if (window.CommonUtils) return window.CommonUtils.formatDate(date) || null;
|
||||
if (!date) return null;
|
||||
|
||||
let dateObj;
|
||||
if (date instanceof Date) {
|
||||
dateObj = date;
|
||||
} else if (typeof date === 'string') {
|
||||
// 문자열인 경우 Date 객체로 변환
|
||||
dateObj = new Date(date);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 로컬 시간대 기준으로 날짜 추출 (UTC 변환 방지)
|
||||
const year = dateObj.getFullYear();
|
||||
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(dateObj.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 정보 가져오기 (auth-check.js와 동일한 로직)
|
||||
*/
|
||||
function getUser() {
|
||||
const user = localStorage.getItem('sso_user');
|
||||
return user ? JSON.parse(user) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 유형에 따른 기본 작업시간 반환
|
||||
*/
|
||||
function getDefaultHoursFromAttendance(tbm) {
|
||||
// work_hours가 있으면 (분할 배정) 해당 값 우선 사용
|
||||
if (tbm.work_hours != null && parseFloat(tbm.work_hours) > 0) {
|
||||
return parseFloat(tbm.work_hours);
|
||||
}
|
||||
switch (tbm.attendance_type) {
|
||||
case 'overtime': return 8 + (parseFloat(tbm.attendance_hours) || 0);
|
||||
case 'regular': return 8;
|
||||
case 'half': return 4;
|
||||
case 'quarter': return 6;
|
||||
case 'early': return parseFloat(tbm.attendance_hours) || 0;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 유형 뱃지 HTML 반환
|
||||
*/
|
||||
function getAttendanceBadgeHtml(type) {
|
||||
const labels = { overtime: '연장근무', regular: '정시근로', annual: '연차', half: '반차', quarter: '반반차', early: '조퇴' };
|
||||
const colors = { overtime: '#7c3aed', regular: '#2563eb', annual: '#ef4444', half: '#f59e0b', quarter: '#f97316', early: '#6b7280' };
|
||||
if (!type || !labels[type]) return '';
|
||||
return ` <span style="display:inline-block; padding:0.125rem 0.375rem; border-radius:0.25rem; font-size:0.625rem; font-weight:700; color:white; background:${colors[type]}; vertical-align:middle; margin-left:0.25rem;">${labels[type]}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 시간 표시 포맷
|
||||
*/
|
||||
function formatHoursDisplay(val) {
|
||||
if (!val || val <= 0) return '시간 선택';
|
||||
val = parseFloat(val);
|
||||
if (val === Math.floor(val)) return val + '시간';
|
||||
const hours = Math.floor(val);
|
||||
const mins = Math.round((val - hours) * 60);
|
||||
return hours > 0 ? hours + '시간 ' + mins + '분' : mins + '분';
|
||||
if (window.getSSOUser) return window.getSSOUser();
|
||||
const raw = localStorage.getItem('sso_user') || localStorage.getItem('user');
|
||||
try { return raw ? JSON.parse(raw) : null; } catch(e) { return null; }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,7 +209,7 @@ function renderTbmWorkList() {
|
||||
byDate[dateStr].sessions[sessionKey] = {
|
||||
session_id: tbm.session_id,
|
||||
session_date: tbm.session_date,
|
||||
created_by_name: tbm.created_by_name,
|
||||
created_by_name: tbm.leader_name || tbm.created_by_name || '-',
|
||||
items: []
|
||||
};
|
||||
}
|
||||
@@ -462,34 +393,29 @@ function renderTbmWorkList() {
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// 근태 기반 자동 시간 채움
|
||||
const defaultHours = tbm.attendance_type ? getDefaultHoursFromAttendance(tbm) : 0;
|
||||
const hasDefaultHours = defaultHours > 0;
|
||||
const attendanceBadgeHtml = tbm.attendance_type ? getAttendanceBadgeHtml(tbm.attendance_type) : '';
|
||||
return `
|
||||
<tr data-index="${index}" data-type="tbm" data-session-key="${key}">
|
||||
<td>
|
||||
<div class="worker-cell">
|
||||
<strong>${tbm.worker_name || '작업자'}</strong>${attendanceBadgeHtml}
|
||||
<strong>${tbm.worker_name || '작업자'}</strong>
|
||||
<div class="worker-job-type">${tbm.job_type || '-'}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>${tbm.project_name || '-'}</td>
|
||||
<td>${tbm.work_type_name || '-'}</td>
|
||||
<td>${tbm.task_name || '-'}</td>
|
||||
<td>
|
||||
<td data-label="프로젝트">${tbm.project_name || '-'}</td>
|
||||
<td data-label="공정">${tbm.work_type_name || '-'}</td>
|
||||
<td data-label="작업">${tbm.task_name || '-'}</td>
|
||||
<td data-label="작업장소">
|
||||
<div class="workplace-cell">
|
||||
<div>${tbm.category_name || ''}</div>
|
||||
<div>${tbm.workplace_name || '-'}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" id="totalHours_${index}" value="${hasDefaultHours ? defaultHours : ''}" required>
|
||||
<div class="time-input-trigger ${hasDefaultHours ? '' : 'placeholder'}"
|
||||
<input type="hidden" id="totalHours_${index}" value="" required>
|
||||
<div class="time-input-trigger placeholder"
|
||||
id="totalHoursDisplay_${index}"
|
||||
onclick="openTimePicker(${index}, 'total')"
|
||||
style="${hasDefaultHours ? 'color:#1f2937; font-weight:600;' : ''}">
|
||||
${hasDefaultHours ? formatHoursDisplay(defaultHours) : '시간 선택'}
|
||||
onclick="openTimePicker(${index}, 'total')">
|
||||
시간 선택
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@@ -593,6 +519,10 @@ window.calculateRegularHours = function(index) {
|
||||
* TBM 작업보고서 제출
|
||||
*/
|
||||
window.submitTbmWorkReport = async function(index) {
|
||||
// busy guard - 중복 제출 방지
|
||||
const submitBtn = document.querySelector(`tr[data-index="${index}"][data-type="tbm"] .btn-submit-compact`);
|
||||
if (submitBtn && submitBtn.classList.contains('is-loading')) return;
|
||||
|
||||
const tbm = incompleteTbms[index];
|
||||
|
||||
const totalHours = parseFloat(document.getElementById(`totalHours_${index}`).value);
|
||||
@@ -614,6 +544,13 @@ window.submitTbmWorkReport = async function(index) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 로딩 상태 시작
|
||||
if (submitBtn) {
|
||||
submitBtn.classList.add('is-loading');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '제출 중';
|
||||
}
|
||||
|
||||
// 부적합 원인 유효성 검사 (issue_report_id 또는 category_id 또는 error_type_id 필요)
|
||||
console.log('🔍 부적합 검증 시작:', defects.map(d => ({
|
||||
defect_hours: d.defect_hours,
|
||||
@@ -722,6 +659,13 @@ window.submitTbmWorkReport = async function(index) {
|
||||
} catch (error) {
|
||||
console.error('TBM 작업보고서 제출 오류:', error);
|
||||
showSaveResultModal('error', '제출 실패', error.message);
|
||||
} finally {
|
||||
// 로딩 상태 해제
|
||||
if (submitBtn) {
|
||||
submitBtn.classList.remove('is-loading');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '제출';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -729,6 +673,10 @@ window.submitTbmWorkReport = async function(index) {
|
||||
* TBM 세션 일괄제출
|
||||
*/
|
||||
window.batchSubmitTbmSession = async function(sessionKey) {
|
||||
// busy guard - 일괄제출 버튼
|
||||
const batchBtn = document.querySelector(`[data-session-key="${sessionKey}"] ~ .batch-submit-container .btn-batch-submit, .tbm-session-group[data-session-key="${sessionKey}"] .btn-batch-submit`);
|
||||
if (batchBtn && batchBtn.classList.contains('is-loading')) return;
|
||||
|
||||
// 해당 세션의 모든 항목 가져오기
|
||||
const sessionRows = document.querySelectorAll(`tr[data-session-key="${sessionKey}"]`);
|
||||
|
||||
@@ -804,7 +752,8 @@ window.batchSubmitTbmSession = async function(sessionKey) {
|
||||
}
|
||||
|
||||
// 2단계: 모든 항목 제출
|
||||
const submitBtn = event.target;
|
||||
const submitBtn = batchBtn || event.target;
|
||||
submitBtn.classList.add('is-loading');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '제출 중...';
|
||||
|
||||
@@ -869,6 +818,7 @@ window.batchSubmitTbmSession = async function(sessionKey) {
|
||||
console.error('일괄제출 오류:', error);
|
||||
showSaveResultModal('error', '일괄제출 오류', error.message);
|
||||
} finally {
|
||||
submitBtn.classList.remove('is-loading');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = `📤 이 세션 일괄제출 (${sessionRows.length}건)`;
|
||||
}
|
||||
@@ -1160,7 +1110,7 @@ async function loadWorkplaceMap(categoryId, layoutImagePath, workplaces) {
|
||||
mapCtx = mapCanvas.getContext('2d');
|
||||
|
||||
// 이미지 URL 생성
|
||||
const baseUrl = window.API_BASE_URL || 'http://localhost:30005';
|
||||
const baseUrl = window.API_BASE_URL || 'http://localhost:20005';
|
||||
const apiBaseUrl = baseUrl.replace('/api', ''); // /api 제거
|
||||
const fullImageUrl = layoutImagePath.startsWith('http')
|
||||
? layoutImagePath
|
||||
@@ -1690,12 +1640,8 @@ window.submitAllManualWorkReports = async function() {
|
||||
* 날짜 포맷 함수
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
if (window.CommonUtils) return window.CommonUtils.formatDate(dateString);
|
||||
return formatDateForApi(dateString);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2015,38 +1961,36 @@ window.deleteWorkReport = async function(reportId) {
|
||||
// 기존 함수들
|
||||
// =================================================================
|
||||
|
||||
// 한국 시간 기준 오늘 날짜 가져오기
|
||||
// 한국 시간 기준 오늘 날짜
|
||||
function getKoreaToday() {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
if (window.CommonUtils) return window.CommonUtils.getTodayKST();
|
||||
const now = new Date();
|
||||
return now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0');
|
||||
}
|
||||
|
||||
// 현재 로그인한 사용자 정보 가져오기
|
||||
function getCurrentUser() {
|
||||
try {
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token) return null;
|
||||
// SSO 사용자 정보 우선
|
||||
if (window.getSSOUser) {
|
||||
const ssoUser = window.getSSOUser();
|
||||
if (ssoUser) return ssoUser;
|
||||
}
|
||||
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
if (payloadBase64) {
|
||||
const payload = JSON.parse(atob(payloadBase64));
|
||||
console.log('토큰에서 추출한 사용자 정보:', payload);
|
||||
return payload;
|
||||
try {
|
||||
const token = window.getSSOToken ? window.getSSOToken() : (localStorage.getItem('sso_token') || localStorage.getItem('token'));
|
||||
if (token) {
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
if (payloadBase64) {
|
||||
return JSON.parse(atob(payloadBase64));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('토큰에서 사용자 정보 추출 실패:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
|
||||
if (userInfo) {
|
||||
const parsed = JSON.parse(userInfo);
|
||||
console.log('localStorage에서 가져온 사용자 정보:', parsed);
|
||||
return parsed;
|
||||
}
|
||||
const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('user') || localStorage.getItem('userInfo');
|
||||
if (userInfo) return JSON.parse(userInfo);
|
||||
} catch (error) {
|
||||
console.log('localStorage에서 사용자 정보 가져오기 실패:', error);
|
||||
}
|
||||
@@ -3183,14 +3127,15 @@ function setupEventListeners() {
|
||||
// 초기화
|
||||
async function init() {
|
||||
try {
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token || token === 'undefined') {
|
||||
showMessage('로그인이 필요합니다.', 'error');
|
||||
localStorage.removeItem('sso_token');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 2000);
|
||||
return;
|
||||
// app-init.js(defer)가 토큰/apiCall 설정 완료할 때까지 대기
|
||||
if (window.waitForApi) {
|
||||
await window.waitForApi(8000);
|
||||
} else if (!window.apiCall) {
|
||||
// waitForApi 없으면 간단 폴링
|
||||
await new Promise((resolve, reject) => {
|
||||
let elapsed = 0;
|
||||
const iv = setInterval(() => { elapsed += 50; if (window.apiCall) { clearInterval(iv); resolve(); } else if (elapsed >= 8000) { clearInterval(iv); reject(new Error('apiCall timeout')); } }, 50);
|
||||
});
|
||||
}
|
||||
|
||||
await loadData();
|
||||
@@ -3207,8 +3152,12 @@ async function init() {
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 로드 시 초기화
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
// 페이지 로드 시 초기화 (module 스크립트는 DOMContentLoaded 이후 실행될 수 있음)
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// 전역 함수로 노출
|
||||
window.removeWorkEntry = removeWorkEntry;
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
/**
|
||||
* Daily Work Report - Module Loader
|
||||
* 작업보고서 모듈을 초기화하고 연결하는 메인 진입점
|
||||
*
|
||||
* 로드 순서:
|
||||
* 1. state.js - 전역 상태 관리
|
||||
* 2. utils.js - 유틸리티 함수
|
||||
* 3. api.js - API 클라이언트
|
||||
* 4. index.js - 이 파일 (메인 컨트롤러)
|
||||
*/
|
||||
|
||||
class DailyWorkReportController {
|
||||
constructor() {
|
||||
this.state = window.DailyWorkReportState;
|
||||
this.api = window.DailyWorkReportAPI;
|
||||
this.utils = window.DailyWorkReportUtils;
|
||||
this.initialized = false;
|
||||
|
||||
console.log('[Controller] DailyWorkReportController 생성');
|
||||
}
|
||||
|
||||
/**
|
||||
* 초기화
|
||||
*/
|
||||
async init() {
|
||||
if (this.initialized) {
|
||||
console.log('[Controller] 이미 초기화됨');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Controller] 초기화 시작...');
|
||||
|
||||
try {
|
||||
// 이벤트 리스너 설정
|
||||
this.setupEventListeners();
|
||||
|
||||
// 기본 데이터 로드
|
||||
await this.api.loadAllData();
|
||||
|
||||
// TBM 탭이 기본
|
||||
await this.switchTab('tbm');
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[Controller] 초기화 완료');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Controller] 초기화 실패:', error);
|
||||
window.showMessage?.('초기화 중 오류가 발생했습니다: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 리스너 설정
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// 탭 버튼
|
||||
const tbmBtn = document.getElementById('tbmReportTab');
|
||||
const completedBtn = document.getElementById('completedReportTab');
|
||||
|
||||
if (tbmBtn) {
|
||||
tbmBtn.addEventListener('click', () => this.switchTab('tbm'));
|
||||
}
|
||||
if (completedBtn) {
|
||||
completedBtn.addEventListener('click', () => this.switchTab('completed'));
|
||||
}
|
||||
|
||||
// 완료 보고서 날짜 변경
|
||||
const completedDateInput = document.getElementById('completedReportDate');
|
||||
if (completedDateInput) {
|
||||
completedDateInput.addEventListener('change', () => this.loadCompletedReports());
|
||||
}
|
||||
|
||||
console.log('[Controller] 이벤트 리스너 설정 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 전환
|
||||
*/
|
||||
async switchTab(tab) {
|
||||
this.state.setCurrentTab(tab);
|
||||
|
||||
const tbmBtn = document.getElementById('tbmReportTab');
|
||||
const completedBtn = document.getElementById('completedReportTab');
|
||||
const tbmSection = document.getElementById('tbmReportSection');
|
||||
const completedSection = document.getElementById('completedReportSection');
|
||||
|
||||
// 모든 탭 버튼 비활성화
|
||||
tbmBtn?.classList.remove('active');
|
||||
completedBtn?.classList.remove('active');
|
||||
|
||||
// 모든 섹션 숨기기
|
||||
if (tbmSection) tbmSection.style.display = 'none';
|
||||
if (completedSection) completedSection.style.display = 'none';
|
||||
|
||||
// 선택된 탭 활성화
|
||||
if (tab === 'tbm') {
|
||||
tbmBtn?.classList.add('active');
|
||||
if (tbmSection) tbmSection.style.display = 'block';
|
||||
await this.loadTbmData();
|
||||
} else if (tab === 'completed') {
|
||||
completedBtn?.classList.add('active');
|
||||
if (completedSection) completedSection.style.display = 'block';
|
||||
|
||||
// 오늘 날짜로 초기화
|
||||
const dateInput = document.getElementById('completedReportDate');
|
||||
if (dateInput) {
|
||||
dateInput.value = this.utils.getKoreaToday();
|
||||
}
|
||||
await this.loadCompletedReports();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 데이터 로드
|
||||
*/
|
||||
async loadTbmData() {
|
||||
try {
|
||||
await this.api.loadIncompleteTbms();
|
||||
await this.api.loadDailyIssuesForTbms();
|
||||
|
||||
// 렌더링은 기존 함수 사용 (점진적 마이그레이션)
|
||||
if (typeof window.renderTbmWorkList === 'function') {
|
||||
window.renderTbmWorkList();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Controller] TBM 데이터 로드 오류:', error);
|
||||
window.showMessage?.('TBM 데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료 보고서 로드
|
||||
*/
|
||||
async loadCompletedReports() {
|
||||
try {
|
||||
const dateInput = document.getElementById('completedReportDate');
|
||||
const date = dateInput?.value || this.utils.getKoreaToday();
|
||||
|
||||
const reports = await this.api.loadCompletedReports(date);
|
||||
|
||||
// 렌더링은 기존 함수 사용
|
||||
if (typeof window.renderCompletedReports === 'function') {
|
||||
window.renderCompletedReports(reports);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Controller] 완료 보고서 로드 오류:', error);
|
||||
window.showMessage?.('완료 보고서를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 작업보고서 제출
|
||||
*/
|
||||
async submitTbmWorkReport(index) {
|
||||
try {
|
||||
const tbm = this.state.incompleteTbms[index];
|
||||
if (!tbm) {
|
||||
throw new Error('TBM 데이터를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 유효성 검사
|
||||
const totalHoursInput = document.getElementById(`totalHours_${index}`);
|
||||
const totalHours = parseFloat(totalHoursInput?.value);
|
||||
|
||||
if (!totalHours || totalHours <= 0) {
|
||||
window.showMessage?.('작업시간을 입력해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 부적합 시간 계산
|
||||
const defects = this.state.tempDefects[index] || [];
|
||||
const errorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0);
|
||||
const regularHours = totalHours - errorHours;
|
||||
|
||||
if (regularHours < 0) {
|
||||
window.showMessage?.('부적합 시간이 총 작업시간을 초과할 수 없습니다.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// API 데이터 구성
|
||||
const user = this.state.getCurrentUser();
|
||||
const reportData = {
|
||||
tbm_session_id: tbm.session_id,
|
||||
tbm_assignment_id: tbm.assignment_id,
|
||||
worker_id: tbm.worker_id,
|
||||
project_id: tbm.project_id,
|
||||
work_type_id: tbm.work_type_id,
|
||||
report_date: this.utils.formatDateForApi(tbm.session_date),
|
||||
total_hours: totalHours,
|
||||
regular_hours: regularHours,
|
||||
error_hours: errorHours,
|
||||
work_status_id: errorHours > 0 ? 2 : 1,
|
||||
created_by: user?.user_id || user?.id,
|
||||
defects: defects.map(d => ({
|
||||
category_id: d.category_id,
|
||||
item_id: d.item_id,
|
||||
issue_report_id: d.issue_report_id,
|
||||
defect_hours: d.defect_hours,
|
||||
note: d.note
|
||||
}))
|
||||
};
|
||||
|
||||
const result = await this.api.submitTbmWorkReport(reportData);
|
||||
|
||||
window.showSaveResultModal?.(
|
||||
'success',
|
||||
'제출 완료',
|
||||
`${tbm.worker_name}의 작업보고서가 제출되었습니다.`
|
||||
);
|
||||
|
||||
// 목록 새로고침
|
||||
await this.loadTbmData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Controller] 제출 오류:', error);
|
||||
window.showSaveResultModal?.(
|
||||
'error',
|
||||
'제출 실패',
|
||||
error.message || '작업보고서 제출 중 오류가 발생했습니다.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 일괄 제출
|
||||
*/
|
||||
async batchSubmitSession(sessionKey) {
|
||||
const rows = document.querySelectorAll(`tr[data-session-key="${sessionKey}"][data-type="tbm"]`);
|
||||
const indices = [];
|
||||
|
||||
rows.forEach(row => {
|
||||
const index = parseInt(row.dataset.index);
|
||||
const totalHoursInput = document.getElementById(`totalHours_${index}`);
|
||||
if (totalHoursInput?.value && parseFloat(totalHoursInput.value) > 0) {
|
||||
indices.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
if (indices.length === 0) {
|
||||
window.showMessage?.('제출할 항목이 없습니다. 작업시간을 입력해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = confirm(`${indices.length}건의 작업보고서를 일괄 제출하시겠습니까?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const index of indices) {
|
||||
try {
|
||||
await this.submitTbmWorkReport(index);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
console.error(`[Controller] 일괄 제출 오류 (index: ${index}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (failCount === 0) {
|
||||
window.showSaveResultModal?.('success', '일괄 제출 완료', `${successCount}건이 성공적으로 제출되었습니다.`);
|
||||
} else {
|
||||
window.showSaveResultModal?.('warning', '일괄 제출 부분 완료', `성공: ${successCount}건, 실패: ${failCount}건`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 디버그
|
||||
*/
|
||||
debug() {
|
||||
console.log('[Controller] 상태 디버그:');
|
||||
this.state.debug();
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.DailyWorkReportController = new DailyWorkReportController();
|
||||
|
||||
// 하위 호환성: 기존 전역 함수들
|
||||
window.switchTab = (tab) => window.DailyWorkReportController.switchTab(tab);
|
||||
window.submitTbmWorkReport = (index) => window.DailyWorkReportController.submitTbmWorkReport(index);
|
||||
window.batchSubmitTbmSession = (sessionKey) => window.DailyWorkReportController.batchSubmitSession(sessionKey);
|
||||
|
||||
// 사용자 정보 함수
|
||||
window.getUser = () => window.DailyWorkReportState.getUser();
|
||||
window.getCurrentUser = () => window.DailyWorkReportState.getCurrentUser();
|
||||
|
||||
// 날짜 그룹 토글 (UI 함수)
|
||||
window.toggleDateGroup = function(dateStr) {
|
||||
const group = document.querySelector(`.date-group[data-date="${dateStr}"]`);
|
||||
if (!group) return;
|
||||
|
||||
const isExpanded = group.classList.contains('expanded');
|
||||
const content = group.querySelector('.date-group-content');
|
||||
const icon = group.querySelector('.date-toggle-icon');
|
||||
|
||||
if (isExpanded) {
|
||||
group.classList.remove('expanded');
|
||||
group.classList.add('collapsed');
|
||||
if (content) content.style.display = 'none';
|
||||
if (icon) icon.textContent = '▶';
|
||||
} else {
|
||||
group.classList.remove('collapsed');
|
||||
group.classList.add('expanded');
|
||||
if (content) content.style.display = 'block';
|
||||
if (icon) icon.textContent = '▼';
|
||||
}
|
||||
};
|
||||
|
||||
// DOMContentLoaded 이벤트에서 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 약간의 지연 후 초기화 (다른 스크립트 로드 대기)
|
||||
setTimeout(() => {
|
||||
window.DailyWorkReportController.init();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
console.log('[Module] daily-work-report/index.js 로드 완료');
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* Daily Work Report - State Manager
|
||||
* 작업보고서 페이지의 전역 상태 관리
|
||||
* 작업보고서 페이지의 전역 상태 관리 (BaseState 상속)
|
||||
*/
|
||||
|
||||
class DailyWorkReportState {
|
||||
class DailyWorkReportState extends BaseState {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// 마스터 데이터
|
||||
this.workTypes = [];
|
||||
this.workStatusTypes = [];
|
||||
@@ -45,53 +47,9 @@ class DailyWorkReportState {
|
||||
// 캐시
|
||||
this.dailyIssuesCache = {}; // { 'YYYY-MM-DD': [issues] }
|
||||
|
||||
// 리스너
|
||||
this.listeners = new Map();
|
||||
|
||||
console.log('[State] DailyWorkReportState 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 업데이트
|
||||
*/
|
||||
update(key, value) {
|
||||
const prevValue = this[key];
|
||||
this[key] = value;
|
||||
this.notifyListeners(key, value, prevValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리스너 등록
|
||||
*/
|
||||
subscribe(key, callback) {
|
||||
if (!this.listeners.has(key)) {
|
||||
this.listeners.set(key, []);
|
||||
}
|
||||
this.listeners.get(key).push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리스너에게 알림
|
||||
*/
|
||||
notifyListeners(key, newValue, prevValue) {
|
||||
const keyListeners = this.listeners.get(key) || [];
|
||||
keyListeners.forEach(callback => {
|
||||
try {
|
||||
callback(newValue, prevValue);
|
||||
} catch (error) {
|
||||
console.error(`[State] 리스너 오류 (${key}):`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자 정보 가져오기
|
||||
*/
|
||||
getUser() {
|
||||
const user = localStorage.getItem('sso_user');
|
||||
return user ? JSON.parse(user) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰에서 사용자 정보 추출
|
||||
*/
|
||||
|
||||
@@ -1,67 +1,29 @@
|
||||
/**
|
||||
* Daily Work Report - Utilities
|
||||
* 작업보고서 관련 유틸리티 함수들
|
||||
* 작업보고서 관련 유틸리티 함수들 (공통 함수는 CommonUtils에 위임)
|
||||
*/
|
||||
|
||||
class DailyWorkReportUtils {
|
||||
constructor() {
|
||||
this._common = window.CommonUtils;
|
||||
console.log('[Utils] DailyWorkReportUtils 초기화');
|
||||
}
|
||||
|
||||
/**
|
||||
* 한국 시간 기준 오늘 날짜 (YYYY-MM-DD)
|
||||
*/
|
||||
getKoreaToday() {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
// --- CommonUtils 위임 ---
|
||||
getKoreaToday() { return this._common.getTodayKST(); }
|
||||
formatDateForApi(date) { return this._common.formatDate(date); }
|
||||
formatDate(date) { return this._common.formatDate(date) || '-'; }
|
||||
getDayOfWeek(date) { return this._common.getDayOfWeek(date); }
|
||||
isToday(date) { return this._common.isToday(date); }
|
||||
generateUUID() { return this._common.generateUUID(); }
|
||||
escapeHtml(text) { return this._common.escapeHtml(text); }
|
||||
debounce(func, wait) { return this._common.debounce(func, wait); }
|
||||
throttle(func, limit) { return this._common.throttle(func, limit); }
|
||||
deepClone(obj) { return this._common.deepClone(obj); }
|
||||
isEmpty(value) { return this._common.isEmpty(value); }
|
||||
groupBy(array, key) { return this._common.groupBy(array, key); }
|
||||
|
||||
/**
|
||||
* 날짜를 API 형식(YYYY-MM-DD)으로 변환
|
||||
*/
|
||||
formatDateForApi(date) {
|
||||
if (!date) return null;
|
||||
|
||||
let dateObj;
|
||||
if (date instanceof Date) {
|
||||
dateObj = date;
|
||||
} else if (typeof date === 'string') {
|
||||
dateObj = new Date(date);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const year = dateObj.getFullYear();
|
||||
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(dateObj.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 포맷팅 (표시용)
|
||||
*/
|
||||
formatDate(date) {
|
||||
if (!date) return '-';
|
||||
|
||||
let dateObj;
|
||||
if (date instanceof Date) {
|
||||
dateObj = date;
|
||||
} else if (typeof date === 'string') {
|
||||
dateObj = new Date(date);
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const year = dateObj.getFullYear();
|
||||
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(dateObj.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
// --- 작업보고 전용 ---
|
||||
|
||||
/**
|
||||
* 시간 포맷팅 (HH:mm)
|
||||
@@ -104,24 +66,6 @@ class DailyWorkReportUtils {
|
||||
return Number(num).toFixed(decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* 요일 반환
|
||||
*/
|
||||
getDayOfWeek(date) {
|
||||
const days = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
return days[dateObj.getDay()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 오늘인지 확인
|
||||
*/
|
||||
isToday(date) {
|
||||
const today = this.getKoreaToday();
|
||||
const targetDate = this.formatDateForApi(date);
|
||||
return today === targetDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 날짜 사이 일수 계산
|
||||
*/
|
||||
@@ -132,63 +76,6 @@ class DailyWorkReportUtils {
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
/**
|
||||
* 디바운스 함수
|
||||
*/
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 쓰로틀 함수
|
||||
*/
|
||||
throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function(...args) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 이스케이프
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* 객체 깊은 복사
|
||||
*/
|
||||
deepClone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* 빈 값 확인
|
||||
*/
|
||||
isEmpty(value) {
|
||||
if (value === null || value === undefined) return true;
|
||||
if (typeof value === 'string') return value.trim() === '';
|
||||
if (Array.isArray(value)) return value.length === 0;
|
||||
if (typeof value === 'object') return Object.keys(value).length === 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 숫자 유효성 검사
|
||||
*/
|
||||
@@ -249,20 +136,6 @@ class DailyWorkReportUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열 그룹화
|
||||
*/
|
||||
groupBy(array, key) {
|
||||
return array.reduce((result, item) => {
|
||||
const groupKey = typeof key === 'function' ? key(item) : item[key];
|
||||
if (!result[groupKey]) {
|
||||
result[groupKey] = [];
|
||||
}
|
||||
result[groupKey].push(item);
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열 정렬 (다중 키)
|
||||
*/
|
||||
@@ -280,17 +153,6 @@ class DailyWorkReportUtils {
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID 생성
|
||||
*/
|
||||
generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
|
||||
1329
system1-factory/web/js/tbm-mobile.js
Normal file
1329
system1-factory/web/js/tbm-mobile.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,325 +0,0 @@
|
||||
/**
|
||||
* TBM - Module Loader
|
||||
* TBM 모듈을 초기화하고 연결하는 메인 진입점
|
||||
*
|
||||
* 로드 순서:
|
||||
* 1. state.js - 전역 상태 관리
|
||||
* 2. utils.js - 유틸리티 함수
|
||||
* 3. api.js - API 클라이언트
|
||||
* 4. index.js - 이 파일 (메인 컨트롤러)
|
||||
*/
|
||||
|
||||
class TbmController {
|
||||
constructor() {
|
||||
this.state = window.TbmState;
|
||||
this.api = window.TbmAPI;
|
||||
this.utils = window.TbmUtils;
|
||||
this.initialized = false;
|
||||
|
||||
console.log('[TbmController] 생성');
|
||||
}
|
||||
|
||||
/**
|
||||
* 초기화
|
||||
*/
|
||||
async init() {
|
||||
if (this.initialized) {
|
||||
console.log('[TbmController] 이미 초기화됨');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🛠️ TBM 관리 페이지 초기화');
|
||||
|
||||
// API 함수가 로드될 때까지 대기
|
||||
let retryCount = 0;
|
||||
while (!window.apiCall && retryCount < 50) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
retryCount++;
|
||||
}
|
||||
|
||||
if (!window.apiCall) {
|
||||
window.showToast?.('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 오늘 날짜 설정 (서울 시간대 기준)
|
||||
const today = this.utils.getTodayKST();
|
||||
const tbmDateEl = document.getElementById('tbmDate');
|
||||
const sessionDateEl = document.getElementById('sessionDate');
|
||||
if (tbmDateEl) tbmDateEl.value = today;
|
||||
if (sessionDateEl) sessionDateEl.value = today;
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
this.setupEventListeners();
|
||||
|
||||
// 초기 데이터 로드
|
||||
await this.api.loadInitialData();
|
||||
await this.api.loadTodayOnlyTbm();
|
||||
|
||||
// 렌더링
|
||||
this.displayTodayTbmSessions();
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[TbmController] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 리스너 설정
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// 탭 버튼들
|
||||
document.querySelectorAll('.tbm-tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tabName = btn.dataset.tab;
|
||||
if (tabName) this.switchTbmTab(tabName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 전환
|
||||
*/
|
||||
async switchTbmTab(tabName) {
|
||||
this.state.setCurrentTab(tabName);
|
||||
|
||||
// 탭 버튼 활성화 상태 변경
|
||||
document.querySelectorAll('.tbm-tab-btn').forEach(btn => {
|
||||
if (btn.dataset.tab === tabName) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 탭 컨텐츠 표시 변경
|
||||
document.querySelectorAll('.tbm-tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
const tabContent = document.getElementById(`${tabName}-tab`);
|
||||
if (tabContent) tabContent.classList.add('active');
|
||||
|
||||
// 탭에 따라 데이터 로드
|
||||
if (tabName === 'tbm-input') {
|
||||
await this.api.loadTodayOnlyTbm();
|
||||
this.displayTodayTbmSessions();
|
||||
} else if (tabName === 'tbm-manage') {
|
||||
await this.api.loadRecentTbmGroupedByDate();
|
||||
this.displayTbmGroupedByDate();
|
||||
this.updateViewModeIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 오늘의 TBM 세션 표시
|
||||
*/
|
||||
displayTodayTbmSessions() {
|
||||
const grid = document.getElementById('todayTbmGrid');
|
||||
const emptyState = document.getElementById('todayEmptyState');
|
||||
const todayTotalEl = document.getElementById('todayTotalSessions');
|
||||
const todayCompletedEl = document.getElementById('todayCompletedSessions');
|
||||
const todayActiveEl = document.getElementById('todayActiveSessions');
|
||||
|
||||
const sessions = this.state.todaySessions;
|
||||
|
||||
if (sessions.length === 0) {
|
||||
if (grid) grid.innerHTML = '';
|
||||
if (emptyState) emptyState.style.display = 'flex';
|
||||
if (todayTotalEl) todayTotalEl.textContent = '0';
|
||||
if (todayCompletedEl) todayCompletedEl.textContent = '0';
|
||||
if (todayActiveEl) todayActiveEl.textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyState) emptyState.style.display = 'none';
|
||||
|
||||
const completedCount = sessions.filter(s => s.status === 'completed').length;
|
||||
const activeCount = sessions.filter(s => s.status === 'draft').length;
|
||||
|
||||
if (todayTotalEl) todayTotalEl.textContent = sessions.length;
|
||||
if (todayCompletedEl) todayCompletedEl.textContent = completedCount;
|
||||
if (todayActiveEl) todayActiveEl.textContent = activeCount;
|
||||
|
||||
if (grid) {
|
||||
grid.innerHTML = sessions.map(session => this.createSessionCard(session)).join('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜별 그룹으로 TBM 표시
|
||||
*/
|
||||
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(this.state.dateGroupedSessions).sort((a, b) =>
|
||||
new Date(b) - new Date(a)
|
||||
);
|
||||
|
||||
if (sortedDates.length === 0 || this.state.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 = this.state.allLoadedSessions.filter(s => s.status === 'completed').length;
|
||||
if (totalSessionsEl) totalSessionsEl.textContent = this.state.allLoadedSessions.length;
|
||||
if (completedSessionsEl) completedSessionsEl.textContent = completedCount;
|
||||
|
||||
// 날짜별 그룹 HTML 생성
|
||||
const today = this.utils.getTodayKST();
|
||||
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
|
||||
container.innerHTML = sortedDates.map(date => {
|
||||
const sessions = this.state.dateGroupedSessions[date];
|
||||
const dateObj = new Date(date + 'T00:00:00');
|
||||
const dayName = dayNames[dateObj.getDay()];
|
||||
const isToday = date === today;
|
||||
|
||||
const [year, month, day] = date.split('-');
|
||||
const displayDate = `${parseInt(month)}월 ${parseInt(day)}일`;
|
||||
|
||||
return `
|
||||
<div class="tbm-date-group" data-date="${date}">
|
||||
<div class="tbm-date-header ${isToday ? 'today' : ''}" onclick="toggleDateGroup('${date}')">
|
||||
<span class="tbm-date-toggle">▼</span>
|
||||
<span class="tbm-date-title">${displayDate}</span>
|
||||
<span class="tbm-date-day">${dayName}요일</span>
|
||||
${isToday ? '<span class="tbm-today-badge">오늘</span>' : ''}
|
||||
<span class="tbm-date-count">${sessions.length}건</span>
|
||||
</div>
|
||||
<div class="tbm-date-content">
|
||||
<div class="tbm-date-grid">
|
||||
${sessions.map(session => this.createSessionCard(session)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 뷰 모드 표시 업데이트
|
||||
*/
|
||||
updateViewModeIndicator() {
|
||||
const indicator = document.getElementById('viewModeIndicator');
|
||||
const text = document.getElementById('viewModeText');
|
||||
|
||||
if (indicator && text) {
|
||||
if (this.state.isAdminUser()) {
|
||||
indicator.style.display = 'none';
|
||||
} else {
|
||||
indicator.style.display = 'inline-flex';
|
||||
text.textContent = '내 TBM';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 세션 카드 생성
|
||||
*/
|
||||
createSessionCard(session) {
|
||||
const statusBadge = this.utils.getStatusBadge(session.status);
|
||||
|
||||
const leaderName = session.leader_name || session.created_by_name || '작업 책임자';
|
||||
const leaderRole = session.leader_name
|
||||
? (session.leader_job_type || '작업자')
|
||||
: '관리자';
|
||||
|
||||
return `
|
||||
<div class="tbm-session-card" onclick="viewTbmSession(${session.session_id})">
|
||||
<div class="tbm-card-header">
|
||||
<div class="tbm-card-header-top">
|
||||
<div>
|
||||
<h3 class="tbm-card-leader">
|
||||
${leaderName}
|
||||
<span class="tbm-card-leader-role">${leaderRole}</span>
|
||||
</h3>
|
||||
</div>
|
||||
${statusBadge}
|
||||
</div>
|
||||
<div class="tbm-card-date">
|
||||
<span>📅</span>
|
||||
${this.utils.formatDate(session.session_date)} ${session.start_time ? '| ' + session.start_time : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-card-body">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
<div class="tbm-card-info-item">
|
||||
<span class="tbm-card-info-label">작업장</span>
|
||||
<span class="tbm-card-info-value">${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>
|
||||
</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>
|
||||
<button class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="event.stopPropagation(); openSafetyCheckModal(${session.session_id})">
|
||||
✓ 안전 체크
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 디버그
|
||||
*/
|
||||
debug() {
|
||||
console.log('[TbmController] 상태 디버그:');
|
||||
this.state.debug();
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.TbmController = new TbmController();
|
||||
|
||||
// 하위 호환성: 기존 전역 함수들
|
||||
window.switchTbmTab = (tabName) => window.TbmController.switchTbmTab(tabName);
|
||||
window.displayTodayTbmSessions = () => window.TbmController.displayTodayTbmSessions();
|
||||
window.displayTbmGroupedByDate = () => window.TbmController.displayTbmGroupedByDate();
|
||||
window.displayTbmSessions = () => window.TbmController.displayTbmGroupedByDate();
|
||||
window.createSessionCard = (session) => window.TbmController.createSessionCard(session);
|
||||
window.updateViewModeIndicator = () => window.TbmController.updateViewModeIndicator();
|
||||
|
||||
// 날짜 그룹 토글
|
||||
window.toggleDateGroup = function(date) {
|
||||
const group = document.querySelector(`.tbm-date-group[data-date="${date}"]`);
|
||||
if (group) {
|
||||
group.classList.toggle('collapsed');
|
||||
}
|
||||
};
|
||||
|
||||
// DOMContentLoaded 이벤트에서 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(() => {
|
||||
window.TbmController.init();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
console.log('[Module] tbm/index.js 로드 완료');
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* TBM - State Manager
|
||||
* TBM 페이지의 전역 상태 관리
|
||||
* TBM 페이지의 전역 상태 관리 (BaseState 상속)
|
||||
*/
|
||||
|
||||
class TbmState {
|
||||
class TbmState extends BaseState {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// 세션 데이터
|
||||
this.allSessions = [];
|
||||
this.todaySessions = [];
|
||||
@@ -48,56 +50,9 @@ class TbmState {
|
||||
this.mapImage = null;
|
||||
this.mapRegions = [];
|
||||
|
||||
// 리스너
|
||||
this.listeners = new Map();
|
||||
|
||||
console.log('[TbmState] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 업데이트
|
||||
*/
|
||||
update(key, value) {
|
||||
const prevValue = this[key];
|
||||
this[key] = value;
|
||||
this.notifyListeners(key, value, prevValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리스너 등록
|
||||
*/
|
||||
subscribe(key, callback) {
|
||||
if (!this.listeners.has(key)) {
|
||||
this.listeners.set(key, []);
|
||||
}
|
||||
this.listeners.get(key).push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리스너 알림
|
||||
*/
|
||||
notifyListeners(key, newValue, prevValue) {
|
||||
const keyListeners = this.listeners.get(key) || [];
|
||||
keyListeners.forEach(callback => {
|
||||
try {
|
||||
callback(newValue, prevValue);
|
||||
} catch (error) {
|
||||
console.error(`[TbmState] 리스너 오류 (${key}):`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자 정보 가져오기
|
||||
*/
|
||||
getUser() {
|
||||
if (!this.currentUser) {
|
||||
const userInfo = localStorage.getItem('sso_user');
|
||||
this.currentUser = userInfo ? JSON.parse(userInfo) : null;
|
||||
}
|
||||
return this.currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin 여부 확인
|
||||
*/
|
||||
@@ -135,7 +90,7 @@ class TbmState {
|
||||
*/
|
||||
createEmptyTaskLine() {
|
||||
return {
|
||||
task_line_id: this.generateUUID(),
|
||||
task_line_id: window.CommonUtils.generateUUID(),
|
||||
project_id: null,
|
||||
work_type_id: null,
|
||||
task_id: null,
|
||||
@@ -207,7 +162,7 @@ class TbmState {
|
||||
this.allLoadedSessions = [];
|
||||
|
||||
sessions.forEach(session => {
|
||||
const date = this.formatDate(session.session_date);
|
||||
const date = window.CommonUtils.formatDate(session.session_date);
|
||||
if (!this.dateGroupedSessions[date]) {
|
||||
this.dateGroupedSessions[date] = [];
|
||||
}
|
||||
@@ -216,32 +171,6 @@ class TbmState {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 포맷팅
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
||||
return dateString;
|
||||
}
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID 생성
|
||||
*/
|
||||
generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 초기화
|
||||
*/
|
||||
|
||||
@@ -1,46 +1,23 @@
|
||||
/**
|
||||
* TBM - Utilities
|
||||
* TBM 관련 유틸리티 함수들
|
||||
* TBM 관련 유틸리티 함수들 (공통 함수는 CommonUtils에 위임)
|
||||
*/
|
||||
|
||||
class TbmUtils {
|
||||
constructor() {
|
||||
this._common = window.CommonUtils;
|
||||
console.log('[TbmUtils] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 서울 시간대(Asia/Seoul, UTC+9) 기준 오늘 날짜를 YYYY-MM-DD 형식으로 반환
|
||||
*/
|
||||
getTodayKST() {
|
||||
const now = new Date();
|
||||
const kstOffset = 9 * 60;
|
||||
const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
|
||||
const kstTime = new Date(utc + (kstOffset * 60000));
|
||||
// --- CommonUtils 위임 ---
|
||||
getTodayKST() { return this._common.getTodayKST(); }
|
||||
formatDate(dateString) { return this._common.formatDate(dateString); }
|
||||
getDayOfWeek(dateString) { return this._common.getDayOfWeek(dateString); }
|
||||
isToday(dateString) { return this._common.isToday(dateString); }
|
||||
generateUUID() { return this._common.generateUUID(); }
|
||||
escapeHtml(text) { return this._common.escapeHtml(text); }
|
||||
|
||||
const year = kstTime.getFullYear();
|
||||
const month = String(kstTime.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(kstTime.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO 날짜 문자열을 YYYY-MM-DD 형식으로 변환
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
||||
return dateString;
|
||||
}
|
||||
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
// --- TBM 전용 ---
|
||||
|
||||
/**
|
||||
* 날짜 표시용 포맷 (MM월 DD일)
|
||||
@@ -56,30 +33,11 @@ class TbmUtils {
|
||||
*/
|
||||
formatDateFull(dateString) {
|
||||
if (!dateString) return '';
|
||||
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const [year, month, day] = dateString.split('-');
|
||||
const dateObj = new Date(dateString);
|
||||
const dayName = dayNames[dateObj.getDay()];
|
||||
const dayName = this._common.getDayOfWeek(dateString);
|
||||
return `${year}년 ${parseInt(month)}월 ${parseInt(day)}일 (${dayName})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요일 반환
|
||||
*/
|
||||
getDayOfWeek(dateString) {
|
||||
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const dateObj = new Date(dateString + 'T00:00:00');
|
||||
return dayNames[dateObj.getDay()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 오늘인지 확인
|
||||
*/
|
||||
isToday(dateString) {
|
||||
const today = this.getTodayKST();
|
||||
return this.formatDate(dateString) === today;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 시간을 HH:MM 형식으로 반환
|
||||
*/
|
||||
@@ -87,40 +45,13 @@ class TbmUtils {
|
||||
return new Date().toTimeString().slice(0, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID 생성
|
||||
*/
|
||||
generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 이스케이프
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨 조건명 반환
|
||||
*/
|
||||
getWeatherConditionName(code) {
|
||||
const names = {
|
||||
clear: '맑음',
|
||||
rain: '비',
|
||||
snow: '눈',
|
||||
heat: '폭염',
|
||||
cold: '한파',
|
||||
wind: '강풍',
|
||||
fog: '안개',
|
||||
dust: '미세먼지'
|
||||
clear: '맑음', rain: '비', snow: '눈', heat: '폭염',
|
||||
cold: '한파', wind: '강풍', fog: '안개', dust: '미세먼지'
|
||||
};
|
||||
return names[code] || code;
|
||||
}
|
||||
@@ -130,14 +61,8 @@ class TbmUtils {
|
||||
*/
|
||||
getWeatherIcon(code) {
|
||||
const icons = {
|
||||
clear: '☀️',
|
||||
rain: '🌧️',
|
||||
snow: '❄️',
|
||||
heat: '🔥',
|
||||
cold: '🥶',
|
||||
wind: '💨',
|
||||
fog: '🌫️',
|
||||
dust: '😷'
|
||||
clear: '☀️', rain: '🌧️', snow: '❄️', heat: '🔥',
|
||||
cold: '🥶', wind: '💨', fog: '🌫️', dust: '😷'
|
||||
};
|
||||
return icons[code] || '🌤️';
|
||||
}
|
||||
@@ -147,12 +72,9 @@ class TbmUtils {
|
||||
*/
|
||||
getCategoryName(category) {
|
||||
const names = {
|
||||
'PPE': '개인 보호 장비',
|
||||
'EQUIPMENT': '장비 점검',
|
||||
'ENVIRONMENT': '작업 환경',
|
||||
'EMERGENCY': '비상 대응',
|
||||
'WEATHER': '날씨',
|
||||
'TASK': '작업'
|
||||
'PPE': '개인 보호 장비', 'EQUIPMENT': '장비 점검',
|
||||
'ENVIRONMENT': '작업 환경', 'EMERGENCY': '비상 대응',
|
||||
'WEATHER': '날씨', 'TASK': '작업'
|
||||
};
|
||||
return names[category] || category;
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
// /js/work-report-api.js
|
||||
import { apiGet, apiPost } from './api-helper.js';
|
||||
|
||||
/**
|
||||
* 작업 보고서 작성을 위해 필요한 초기 데이터(작업자, 프로젝트, 태스크)를 가져옵니다.
|
||||
* Promise.all을 사용하여 병렬로 API를 호출합니다.
|
||||
* @returns {Promise<{workers: Array, projects: Array, tasks: Array}>}
|
||||
*/
|
||||
export async function getInitialData() {
|
||||
try {
|
||||
const [allWorkers, projects, tasks] = await Promise.all([
|
||||
apiGet('/workers'),
|
||||
apiGet('/projects'),
|
||||
apiGet('/tasks')
|
||||
]);
|
||||
|
||||
// 활성화된 작업자만 필터링
|
||||
const workers = allWorkers.filter(worker => {
|
||||
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
|
||||
});
|
||||
|
||||
// 데이터 형식 검증
|
||||
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks)) {
|
||||
throw new Error('서버에서 받은 데이터 형식이 올바르지 않습니다.');
|
||||
}
|
||||
|
||||
// 작업자 목록은 ID 기준으로 정렬
|
||||
workers.sort((a, b) => a.worker_id - b.worker_id);
|
||||
|
||||
return { workers, projects, tasks };
|
||||
} catch (error) {
|
||||
console.error('초기 데이터 로딩 중 오류 발생:', error);
|
||||
// 에러를 다시 던져서 호출한 쪽에서 처리할 수 있도록 함
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작성된 작업 보고서 데이터를 서버에 전송합니다.
|
||||
* @param {Array<object>} reportData - 전송할 작업 보고서 데이터 배열
|
||||
* @returns {Promise<object>} - 서버의 응답 결과
|
||||
*/
|
||||
export async function createWorkReport(reportData) {
|
||||
try {
|
||||
const result = await apiPost('/workreports', reportData);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('작업 보고서 생성 요청 실패:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// /js/work-report-create.js
|
||||
import { renderCalendar } from './calendar.js';
|
||||
import { getInitialData, createWorkReport } from './work-report-api.js';
|
||||
import { initializeReportTable, getReportData } from './work-report-ui.js';
|
||||
|
||||
// 전역 상태 변수
|
||||
let selectedDate = '';
|
||||
|
||||
/**
|
||||
* 날짜가 선택되었을 때 실행되는 콜백 함수.
|
||||
* 초기 데이터를 로드하고 테이블을 렌더링합니다.
|
||||
* @param {string} date - 선택된 날짜 (YYYY-MM-DD 형식)
|
||||
*/
|
||||
async function onDateSelect(date) {
|
||||
selectedDate = date;
|
||||
const tableBody = document.getElementById('reportBody');
|
||||
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">데이터를 불러오는 중...</td></tr>';
|
||||
|
||||
try {
|
||||
const initialData = await getInitialData();
|
||||
initializeReportTable(initialData);
|
||||
} catch (error) {
|
||||
alert('데이터를 불러오는 데 실패했습니다: ' + error.message);
|
||||
tableBody.innerHTML = '<tr><td colspan="8" class="text-center error">오류 발생! 데이터를 불러올 수 없습니다.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* '전체 등록' 버튼 클릭 시 실행되는 이벤트 핸들러.
|
||||
* 폼 데이터를 서버에 전송합니다.
|
||||
*/
|
||||
async function handleSubmit() {
|
||||
if (!selectedDate) {
|
||||
alert('먼저 달력에서 날짜를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const reportData = getReportData();
|
||||
if (!reportData) {
|
||||
// getReportData 내부에서 이미 alert으로 사용자에게 알림
|
||||
return;
|
||||
}
|
||||
|
||||
// 각 항목에 선택된 날짜 추가
|
||||
const payload = reportData.map(item => ({ ...item, date: selectedDate }));
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '등록 중...';
|
||||
|
||||
try {
|
||||
const result = await createWorkReport(payload);
|
||||
if (result.success) {
|
||||
alert('✅ 작업 보고서가 성공적으로 등록되었습니다!');
|
||||
// 성공 후 폼을 다시 로드하거나, 다른 페이지로 이동 등의 로직 추가 가능
|
||||
onDateSelect(selectedDate); // 현재 날짜의 폼을 다시 로드
|
||||
} else {
|
||||
throw new Error(result.error || '알 수 없는 오류로 등록에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('❌ 등록 실패: ' + error.message);
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '전체 등록';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 초기화 함수
|
||||
*/
|
||||
function initializePage() {
|
||||
renderCalendar('calendar', onDateSelect);
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.addEventListener('click', handleSubmit);
|
||||
}
|
||||
|
||||
// DOM이 로드되면 페이지 초기화를 시작합니다.
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
@@ -1,141 +0,0 @@
|
||||
// /js/work-report-ui.js
|
||||
|
||||
const DEFAULT_PROJECT_ID = '13'; // 나중에는 API나 설정에서 받아오는 것이 좋음
|
||||
const DEFAULT_TASK_ID = '15';
|
||||
|
||||
/**
|
||||
* 주어진 데이터를 바탕으로 <select> 요소의 <option>들을 생성합니다.
|
||||
* @param {Array<object>} items - 옵션으로 만들 데이터 배열
|
||||
* @param {string} valueField - <option>의 value 속성에 사용할 필드 이름
|
||||
* @param {string} textField - <option>의 텍스트에 사용할 필드 이름
|
||||
* @returns {string} - 생성된 HTML 옵션 문자열
|
||||
*/
|
||||
function createOptions(items, valueField, textField) {
|
||||
return items.map(item => `<option value="${item[valueField]}">${textField(item)}</option>`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블의 모든 행 번호를 다시 매깁니다.
|
||||
* @param {HTMLTableSectionElement} tableBody - tbody 요소
|
||||
*/
|
||||
function updateRowNumbers(tableBody) {
|
||||
tableBody.querySelectorAll('tr').forEach((tr, index) => {
|
||||
tr.cells[0].textContent = index + 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 하나의 작업 보고서 행(tr)을 생성합니다.
|
||||
* @param {object} worker - 작업자 정보
|
||||
* @param {Array} projects - 전체 프로젝트 목록
|
||||
* @param {Array} tasks - 전체 태스크 목록
|
||||
* @param {number} index - 행 번호
|
||||
* @returns {HTMLTableRowElement} - 생성된 tr 요소
|
||||
*/
|
||||
function createReportRow(worker, projects, tasks, index) {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${index + 1}</td>
|
||||
<td>
|
||||
<input type="hidden" name="worker_id" value="${worker.worker_id}">
|
||||
${worker.worker_name}
|
||||
</td>
|
||||
<td><select name="project_id">${createOptions(projects, 'project_id', p => p.project_name)}</select></td>
|
||||
<td><select name="task_id">${createOptions(tasks, 'task_id', t => `${t.category}:${t.subcategory}`)}</select></td>
|
||||
<td>
|
||||
<select name="overtime">
|
||||
<option value="">없음</option>
|
||||
${[1, 2, 3, 4].map(n => `<option>${n}</option>`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select name="work_type">
|
||||
${['근무', '연차', '유급', '반차', '반반차', '조퇴', '휴무'].map(t => `<option>${t}</option>`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" name="memo" placeholder="메모"></td>
|
||||
<td><button type="button" class="remove-btn">x</button></td>
|
||||
`;
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
const workTypeSelect = tr.querySelector('[name="work_type"]');
|
||||
const projectSelect = tr.querySelector('[name="project_id"]');
|
||||
const taskSelect = tr.querySelector('[name="task_id"]');
|
||||
|
||||
workTypeSelect.addEventListener('change', () => {
|
||||
const isDisabled = ['연차', '휴무', '유급'].includes(workTypeSelect.value);
|
||||
projectSelect.disabled = isDisabled;
|
||||
taskSelect.disabled = isDisabled;
|
||||
if (isDisabled) {
|
||||
projectSelect.value = DEFAULT_PROJECT_ID;
|
||||
taskSelect.value = DEFAULT_TASK_ID;
|
||||
}
|
||||
});
|
||||
|
||||
tr.querySelector('.remove-btn').addEventListener('click', () => {
|
||||
tr.remove();
|
||||
updateRowNumbers(tr.parentElement);
|
||||
});
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 보고서 테이블을 초기화하고 데이터를 채웁니다.
|
||||
* @param {{workers: Array, projects: Array, tasks: Array}} initialData - 초기 데이터
|
||||
*/
|
||||
export function initializeReportTable(initialData) {
|
||||
const tableBody = document.getElementById('reportBody');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.innerHTML = ''; // 기존 내용 초기화
|
||||
const { workers, projects, tasks } = initialData;
|
||||
|
||||
if (!workers || workers.length === 0) {
|
||||
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">등록할 작업자 정보가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
workers.forEach((worker, index) => {
|
||||
const row = createReportRow(worker, projects, tasks, index);
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블에서 폼 데이터를 추출하여 배열로 반환합니다.
|
||||
* @returns {Array<object>|null} - 추출된 데이터 배열 또는 유효성 검사 실패 시 null
|
||||
*/
|
||||
export function getReportData() {
|
||||
const tableBody = document.getElementById('reportBody');
|
||||
const rows = tableBody.querySelectorAll('tr');
|
||||
|
||||
if (rows.length === 0 || (rows.length === 1 && rows[0].cells.length < 2)) {
|
||||
alert('등록할 내용이 없습니다.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const reportData = [];
|
||||
const workerIds = new Set();
|
||||
|
||||
for (const tr of rows) {
|
||||
const workerId = tr.querySelector('[name="worker_id"]').value;
|
||||
if (workerIds.has(workerId)) {
|
||||
alert(`오류: 작업자 '${tr.cells[1].textContent.trim()}'가 중복 등록되었습니다.`);
|
||||
return null;
|
||||
}
|
||||
workerIds.add(workerId);
|
||||
|
||||
reportData.push({
|
||||
worker_id: workerId,
|
||||
project_id: tr.querySelector('[name="project_id"]').value,
|
||||
task_id: tr.querySelector('[name="task_id"]').value,
|
||||
overtime_hours: tr.querySelector('[name="overtime"]').value || 0,
|
||||
work_details: tr.querySelector('[name="work_type"]').value,
|
||||
memo: tr.querySelector('[name="memo"]').value
|
||||
});
|
||||
}
|
||||
|
||||
return reportData;
|
||||
}
|
||||
Reference in New Issue
Block a user