security: 보안 강제 시스템 구축 + 하드코딩 비밀번호 제거
보안 감사 결과 CRITICAL 2건, HIGH 5건 발견 → 수정 완료 + 자동화 구축. [보안 수정] - issue-view.js: 하드코딩 비밀번호 → crypto.getRandomValues() 랜덤 생성 - pushSubscriptionController.js: ntfy 비밀번호 → process.env.NTFY_SUB_PASSWORD - DEPLOY-GUIDE.md/PROGRESS.md/migration SQL: 평문 비밀번호 → placeholder - docker-compose.yml/.env.example: NTFY_SUB_PASSWORD 환경변수 추가 [보안 강제 시스템 - 신규] - scripts/security-scan.sh: 8개 규칙 (CRITICAL 2, HIGH 4, MEDIUM 2) 3모드(staged/all/diff), severity, .securityignore, MEDIUM 임계값 - .githooks/pre-commit: 로컬 빠른 피드백 - .githooks/pre-receive-server.sh: Gitea 서버 최종 차단 bypass 거버넌스([SECURITY-BYPASS: 사유] + 사용자 제한 + 로그) - SECURITY-CHECKLIST.md: 10개 카테고리 자동/수동 구분 - docs/SECURITY-GUIDE.md: 운영자 가이드 (워크플로우, bypass, FAQ) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,386 +0,0 @@
|
||||
/**
|
||||
* Daily Work Report - API Client
|
||||
* 작업보고서 관련 모든 API 호출을 관리
|
||||
*/
|
||||
|
||||
class DailyWorkReportAPI {
|
||||
constructor() {
|
||||
this.state = window.DailyWorkReportState;
|
||||
console.log('[API] DailyWorkReportAPI 초기화');
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업자 로드 (생산팀 소속)
|
||||
*/
|
||||
async loadWorkers() {
|
||||
try {
|
||||
console.log('[API] Workers 로딩 중...');
|
||||
const data = await window.apiCall('/workers?limit=1000&department_id=1');
|
||||
const allWorkers = Array.isArray(data) ? data : (data.data || data.workers || []);
|
||||
|
||||
// 퇴사자만 제외
|
||||
const filtered = allWorkers.filter(worker => worker.employment_status !== 'resigned');
|
||||
|
||||
this.state.workers = filtered;
|
||||
console.log(`[API] Workers 로드 완료: ${filtered.length}명`);
|
||||
return filtered;
|
||||
} catch (error) {
|
||||
console.error('[API] 작업자 로딩 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로젝트 로드 (활성 프로젝트만)
|
||||
*/
|
||||
async loadProjects() {
|
||||
try {
|
||||
console.log('[API] Projects 로딩 중...');
|
||||
const data = await window.apiCall('/projects/active/list');
|
||||
const projects = Array.isArray(data) ? data : (data.data || data.projects || []);
|
||||
|
||||
this.state.projects = projects;
|
||||
console.log(`[API] Projects 로드 완료: ${projects.length}개`);
|
||||
return projects;
|
||||
} catch (error) {
|
||||
console.error('[API] 프로젝트 로딩 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 유형 로드
|
||||
*/
|
||||
async loadWorkTypes() {
|
||||
try {
|
||||
const data = await window.apiCall('/daily-work-reports/work-types');
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
this.state.workTypes = data;
|
||||
console.log('[API] 작업 유형 로드 완료:', data.length);
|
||||
return data;
|
||||
}
|
||||
throw new Error('API 실패');
|
||||
} catch (error) {
|
||||
console.log('[API] 작업 유형 API 사용 불가, 기본값 사용');
|
||||
this.state.workTypes = [
|
||||
{ id: 1, name: 'Base' },
|
||||
{ id: 2, name: 'Vessel' },
|
||||
{ id: 3, name: 'Piping' }
|
||||
];
|
||||
return this.state.workTypes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 업무 상태 유형 로드
|
||||
*/
|
||||
async loadWorkStatusTypes() {
|
||||
try {
|
||||
const data = await window.apiCall('/daily-work-reports/work-status-types');
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
this.state.workStatusTypes = data;
|
||||
console.log('[API] 업무 상태 유형 로드 완료:', data.length);
|
||||
return data;
|
||||
}
|
||||
throw new Error('API 실패');
|
||||
} catch (error) {
|
||||
console.log('[API] 업무 상태 유형 API 사용 불가, 기본값 사용');
|
||||
this.state.workStatusTypes = [
|
||||
{ id: 1, name: '정상', is_error: false },
|
||||
{ id: 2, name: '부적합', is_error: true }
|
||||
];
|
||||
return this.state.workStatusTypes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 오류 유형 로드 (신고 카테고리/아이템)
|
||||
*/
|
||||
async loadErrorTypes() {
|
||||
try {
|
||||
// 1. 신고 카테고리 (nonconformity만)
|
||||
const categoriesResponse = await window.apiCall('/work-issues/categories');
|
||||
if (categoriesResponse.success && categoriesResponse.data) {
|
||||
this.state.issueCategories = categoriesResponse.data.filter(
|
||||
c => c.category_type === 'nonconformity'
|
||||
);
|
||||
console.log('[API] 신고 카테고리 로드:', this.state.issueCategories.length);
|
||||
}
|
||||
|
||||
// 2. 신고 아이템 전체
|
||||
const itemsResponse = await window.apiCall('/work-issues/items');
|
||||
if (itemsResponse.success && itemsResponse.data) {
|
||||
// nonconformity 카테고리의 아이템만 필터링
|
||||
const nonconfCatIds = this.state.issueCategories.map(c => c.category_id);
|
||||
this.state.issueItems = itemsResponse.data.filter(
|
||||
item => nonconfCatIds.includes(item.category_id)
|
||||
);
|
||||
console.log('[API] 신고 아이템 로드:', this.state.issueItems.length);
|
||||
}
|
||||
|
||||
// 레거시 호환: errorTypes에 카테고리 매핑
|
||||
this.state.errorTypes = this.state.issueCategories.map(cat => ({
|
||||
id: cat.category_id,
|
||||
name: cat.category_name
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
console.error('[API] 오류 유형 로딩 오류:', error);
|
||||
// 기본값 설정
|
||||
this.state.errorTypes = [
|
||||
{ id: 1, name: '자재 부적합' },
|
||||
{ id: 2, name: '도면 오류' },
|
||||
{ id: 3, name: '장비 고장' }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미완료 TBM 세션 로드
|
||||
*/
|
||||
async loadIncompleteTbms() {
|
||||
try {
|
||||
const response = await window.apiCall('/tbm/sessions/incomplete-reports');
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || '미완료 TBM 조회 실패');
|
||||
}
|
||||
|
||||
let data = response.data || [];
|
||||
|
||||
// 사용자 권한 확인 및 필터링
|
||||
const user = this.state.getUser();
|
||||
if (user && user.role !== 'Admin' && user.access_level !== 'system') {
|
||||
const userId = user.user_id;
|
||||
data = data.filter(tbm => tbm.created_by === userId);
|
||||
}
|
||||
|
||||
this.state.incompleteTbms = data;
|
||||
console.log('[API] 미완료 TBM 로드 완료:', data.length);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[API] 미완료 TBM 로드 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 세션별 당일 신고 로드
|
||||
*/
|
||||
async loadDailyIssuesForTbms() {
|
||||
const tbms = this.state.incompleteTbms;
|
||||
if (!tbms || tbms.length === 0) {
|
||||
console.log('[API] 미완료 TBM 없음, 신고 조회 건너뜀');
|
||||
return;
|
||||
}
|
||||
|
||||
// 고유한 날짜 수집
|
||||
const uniqueDates = [...new Set(tbms.map(tbm => {
|
||||
return window.DailyWorkReportUtils?.formatDateForApi(tbm.session_date) ||
|
||||
this.formatDateForApi(tbm.session_date);
|
||||
}).filter(Boolean))];
|
||||
|
||||
console.log('[API] 조회할 날짜들:', uniqueDates);
|
||||
|
||||
for (const dateStr of uniqueDates) {
|
||||
if (this.state.dailyIssuesCache[dateStr]) {
|
||||
console.log(`[API] 캐시 사용 (${dateStr})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(`/work-issues?start_date=${dateStr}&end_date=${dateStr}`);
|
||||
if (response.success) {
|
||||
this.state.setDailyIssuesCache(dateStr, response.data || []);
|
||||
console.log(`[API] 신고 로드 완료 (${dateStr}):`, this.state.dailyIssuesCache[dateStr].length);
|
||||
} else {
|
||||
this.state.setDailyIssuesCache(dateStr, []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[API] 신고 조회 오류 (${dateStr}):`, error);
|
||||
this.state.setDailyIssuesCache(dateStr, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료된 작업보고서 조회
|
||||
*/
|
||||
async loadCompletedReports(date) {
|
||||
try {
|
||||
const response = await window.apiCall(`/daily-work-reports/v2/reports?date=${date}`);
|
||||
if (response.success) {
|
||||
console.log(`[API] 완료 보고서 로드 (${date}):`, response.data?.length || 0);
|
||||
return response.data || [];
|
||||
}
|
||||
throw new Error(response.message || '조회 실패');
|
||||
} catch (error) {
|
||||
console.error('[API] 완료 보고서 로드 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 작업보고서 제출
|
||||
*/
|
||||
async submitTbmWorkReport(reportData) {
|
||||
try {
|
||||
const response = await window.apiCall('/daily-work-reports/from-tbm', 'POST', reportData);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || '제출 실패');
|
||||
}
|
||||
console.log('[API] TBM 작업보고서 제출 완료:', response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[API] TBM 작업보고서 제출 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 작업보고서 제출
|
||||
*/
|
||||
async submitManualWorkReport(reportData) {
|
||||
try {
|
||||
const response = await window.apiCall('/daily-work-reports/v2/reports', 'POST', reportData);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || '제출 실패');
|
||||
}
|
||||
console.log('[API] 수동 작업보고서 제출 완료:', response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[API] 수동 작업보고서 제출 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업보고서 삭제
|
||||
*/
|
||||
async deleteWorkReport(reportId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/daily-work-reports/v2/reports/${reportId}`, 'DELETE');
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || '삭제 실패');
|
||||
}
|
||||
console.log('[API] 작업보고서 삭제 완료:', reportId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[API] 작업보고서 삭제 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업보고서 수정
|
||||
*/
|
||||
async updateWorkReport(reportId, updateData) {
|
||||
try {
|
||||
const response = await window.apiCall(`/daily-work-reports/v2/reports/${reportId}`, 'PUT', updateData);
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || '수정 실패');
|
||||
}
|
||||
console.log('[API] 작업보고서 수정 완료:', reportId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[API] 작업보고서 수정 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 신고 카테고리 추가
|
||||
*/
|
||||
async addIssueCategory(categoryData) {
|
||||
try {
|
||||
const response = await window.apiCall('/work-issues/categories', 'POST', categoryData);
|
||||
if (response.success) {
|
||||
await this.loadErrorTypes(); // 목록 새로고침
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[API] 카테고리 추가 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 신고 아이템 추가
|
||||
*/
|
||||
async addIssueItem(itemData) {
|
||||
try {
|
||||
const response = await window.apiCall('/work-issues/items', 'POST', itemData);
|
||||
if (response.success) {
|
||||
await this.loadErrorTypes(); // 목록 새로고침
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[API] 아이템 추가 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 기본 데이터 로드
|
||||
*/
|
||||
async loadAllData() {
|
||||
console.log('[API] 모든 기본 데이터 로딩 시작...');
|
||||
|
||||
await Promise.all([
|
||||
this.loadWorkers(),
|
||||
this.loadProjects(),
|
||||
this.loadWorkTypes(),
|
||||
this.loadWorkStatusTypes(),
|
||||
this.loadErrorTypes()
|
||||
]);
|
||||
|
||||
console.log('[API] 모든 기본 데이터 로딩 완료');
|
||||
}
|
||||
|
||||
// 유틸리티: 날짜 형식 변환 (API 형식)
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.DailyWorkReportAPI = new DailyWorkReportAPI();
|
||||
|
||||
// 하위 호환성: 기존 함수들
|
||||
window.loadWorkers = () => window.DailyWorkReportAPI.loadWorkers();
|
||||
window.loadProjects = () => window.DailyWorkReportAPI.loadProjects();
|
||||
window.loadWorkTypes = () => window.DailyWorkReportAPI.loadWorkTypes();
|
||||
window.loadWorkStatusTypes = () => window.DailyWorkReportAPI.loadWorkStatusTypes();
|
||||
window.loadErrorTypes = () => window.DailyWorkReportAPI.loadErrorTypes();
|
||||
window.loadIncompleteTbms = () => window.DailyWorkReportAPI.loadIncompleteTbms();
|
||||
window.loadDailyIssuesForTbms = () => window.DailyWorkReportAPI.loadDailyIssuesForTbms();
|
||||
window.loadCompletedReports = () => window.DailyWorkReportAPI.loadCompletedReports(
|
||||
document.getElementById('completedReportDate')?.value
|
||||
);
|
||||
|
||||
// 통합 데이터 로드 함수
|
||||
window.loadData = async () => {
|
||||
try {
|
||||
window.showMessage?.('데이터를 불러오는 중...', 'loading');
|
||||
await window.DailyWorkReportAPI.loadAllData();
|
||||
window.hideMessage?.();
|
||||
} catch (error) {
|
||||
console.error('[API] 데이터 로드 실패:', error);
|
||||
window.showMessage?.('데이터 로드 중 오류가 발생했습니다: ' + error.message, 'error');
|
||||
}
|
||||
};
|
||||
@@ -1,300 +0,0 @@
|
||||
/**
|
||||
* Daily Work Report - State Manager
|
||||
* 작업보고서 페이지의 전역 상태 관리 (BaseState 상속)
|
||||
*/
|
||||
|
||||
class DailyWorkReportState extends BaseState {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// 마스터 데이터
|
||||
this.workTypes = [];
|
||||
this.workStatusTypes = [];
|
||||
this.errorTypes = []; // 레거시 호환용
|
||||
this.issueCategories = []; // 신고 카테고리 (nonconformity)
|
||||
this.issueItems = []; // 신고 아이템
|
||||
this.workers = [];
|
||||
this.projects = [];
|
||||
|
||||
// UI 상태
|
||||
this.selectedWorkers = new Set();
|
||||
this.workEntryCounter = 0;
|
||||
this.currentStep = 1;
|
||||
this.editingWorkId = null;
|
||||
this.currentTab = 'tbm';
|
||||
|
||||
// TBM 관련
|
||||
this.incompleteTbms = [];
|
||||
|
||||
// 부적합 원인 관리
|
||||
this.currentDefectIndex = null;
|
||||
this.tempDefects = {}; // { index: [{ error_type_id, defect_hours, note }] }
|
||||
|
||||
// 작업장소 지도 관련
|
||||
this.mapCanvas = null;
|
||||
this.mapCtx = null;
|
||||
this.mapImage = null;
|
||||
this.mapRegions = [];
|
||||
this.selectedWorkplace = null;
|
||||
this.selectedWorkplaceName = null;
|
||||
this.selectedWorkplaceCategory = null;
|
||||
this.selectedWorkplaceCategoryName = null;
|
||||
|
||||
// 시간 선택 관련
|
||||
this.currentEditingField = null; // { index, type: 'total' | 'error' }
|
||||
this.currentTimeValue = 0;
|
||||
|
||||
// 캐시
|
||||
this.dailyIssuesCache = {}; // { 'YYYY-MM-DD': [issues] }
|
||||
|
||||
console.log('[State] DailyWorkReportState 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰에서 사용자 정보 추출
|
||||
*/
|
||||
getCurrentUser() {
|
||||
try {
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token) return null;
|
||||
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
if (payloadBase64) {
|
||||
const payload = JSON.parse(atob(payloadBase64));
|
||||
return payload;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[State] 토큰에서 사용자 정보 추출 실패:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = localStorage.getItem('sso_user');
|
||||
if (userInfo) {
|
||||
return JSON.parse(userInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[State] localStorage에서 사용자 정보 가져오기 실패:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택된 작업자 토글
|
||||
*/
|
||||
toggleWorkerSelection(workerId) {
|
||||
if (this.selectedWorkers.has(workerId)) {
|
||||
this.selectedWorkers.delete(workerId);
|
||||
} else {
|
||||
this.selectedWorkers.add(workerId);
|
||||
}
|
||||
this.notifyListeners('selectedWorkers', this.selectedWorkers, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업자 전체 선택/해제
|
||||
*/
|
||||
selectAllWorkers(select = true) {
|
||||
if (select) {
|
||||
this.workers.forEach(w => this.selectedWorkers.add(w.user_id));
|
||||
} else {
|
||||
this.selectedWorkers.clear();
|
||||
}
|
||||
this.notifyListeners('selectedWorkers', this.selectedWorkers, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 항목 카운터 증가
|
||||
*/
|
||||
incrementWorkEntryCounter() {
|
||||
this.workEntryCounter++;
|
||||
return this.workEntryCounter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 변경
|
||||
*/
|
||||
setCurrentTab(tab) {
|
||||
const prevTab = this.currentTab;
|
||||
this.currentTab = tab;
|
||||
this.notifyListeners('currentTab', tab, prevTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* 부적합 임시 저장소 초기화
|
||||
*/
|
||||
initTempDefects(index) {
|
||||
if (!this.tempDefects[index]) {
|
||||
this.tempDefects[index] = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 부적합 추가
|
||||
*/
|
||||
addTempDefect(index, defect) {
|
||||
this.initTempDefects(index);
|
||||
this.tempDefects[index].push(defect);
|
||||
this.notifyListeners('tempDefects', this.tempDefects, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 부적합 업데이트
|
||||
*/
|
||||
updateTempDefect(index, defectIndex, field, value) {
|
||||
if (this.tempDefects[index] && this.tempDefects[index][defectIndex]) {
|
||||
this.tempDefects[index][defectIndex][field] = value;
|
||||
this.notifyListeners('tempDefects', this.tempDefects, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 부적합 삭제
|
||||
*/
|
||||
removeTempDefect(index, defectIndex) {
|
||||
if (this.tempDefects[index]) {
|
||||
this.tempDefects[index].splice(defectIndex, 1);
|
||||
this.notifyListeners('tempDefects', this.tempDefects, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 일일 이슈 캐시 설정
|
||||
*/
|
||||
setDailyIssuesCache(dateStr, issues) {
|
||||
this.dailyIssuesCache[dateStr] = issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* 일일 이슈 캐시 조회
|
||||
*/
|
||||
getDailyIssuesCache(dateStr) {
|
||||
return this.dailyIssuesCache[dateStr] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 초기화
|
||||
*/
|
||||
reset() {
|
||||
this.selectedWorkers.clear();
|
||||
this.workEntryCounter = 0;
|
||||
this.currentStep = 1;
|
||||
this.editingWorkId = null;
|
||||
this.tempDefects = {};
|
||||
this.currentDefectIndex = null;
|
||||
this.dailyIssuesCache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 디버그 출력
|
||||
*/
|
||||
debug() {
|
||||
console.log('[State] 현재 상태:', {
|
||||
workTypes: this.workTypes.length,
|
||||
workers: this.workers.length,
|
||||
projects: this.projects.length,
|
||||
selectedWorkers: this.selectedWorkers.size,
|
||||
currentTab: this.currentTab,
|
||||
incompleteTbms: this.incompleteTbms.length,
|
||||
tempDefects: Object.keys(this.tempDefects).length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.DailyWorkReportState = new DailyWorkReportState();
|
||||
|
||||
// 하위 호환성을 위한 전역 변수 프록시
|
||||
const stateProxy = window.DailyWorkReportState;
|
||||
|
||||
// 기존 전역 변수들과 호환
|
||||
Object.defineProperties(window, {
|
||||
workTypes: {
|
||||
get: () => stateProxy.workTypes,
|
||||
set: (v) => { stateProxy.workTypes = v; }
|
||||
},
|
||||
workStatusTypes: {
|
||||
get: () => stateProxy.workStatusTypes,
|
||||
set: (v) => { stateProxy.workStatusTypes = v; }
|
||||
},
|
||||
errorTypes: {
|
||||
get: () => stateProxy.errorTypes,
|
||||
set: (v) => { stateProxy.errorTypes = v; }
|
||||
},
|
||||
issueCategories: {
|
||||
get: () => stateProxy.issueCategories,
|
||||
set: (v) => { stateProxy.issueCategories = v; }
|
||||
},
|
||||
issueItems: {
|
||||
get: () => stateProxy.issueItems,
|
||||
set: (v) => { stateProxy.issueItems = v; }
|
||||
},
|
||||
workers: {
|
||||
get: () => stateProxy.workers,
|
||||
set: (v) => { stateProxy.workers = v; }
|
||||
},
|
||||
projects: {
|
||||
get: () => stateProxy.projects,
|
||||
set: (v) => { stateProxy.projects = v; }
|
||||
},
|
||||
selectedWorkers: {
|
||||
get: () => stateProxy.selectedWorkers,
|
||||
set: (v) => { stateProxy.selectedWorkers = v; }
|
||||
},
|
||||
incompleteTbms: {
|
||||
get: () => stateProxy.incompleteTbms,
|
||||
set: (v) => { stateProxy.incompleteTbms = v; }
|
||||
},
|
||||
tempDefects: {
|
||||
get: () => stateProxy.tempDefects,
|
||||
set: (v) => { stateProxy.tempDefects = v; }
|
||||
},
|
||||
dailyIssuesCache: {
|
||||
get: () => stateProxy.dailyIssuesCache,
|
||||
set: (v) => { stateProxy.dailyIssuesCache = v; }
|
||||
},
|
||||
currentTab: {
|
||||
get: () => stateProxy.currentTab,
|
||||
set: (v) => { stateProxy.currentTab = v; }
|
||||
},
|
||||
currentStep: {
|
||||
get: () => stateProxy.currentStep,
|
||||
set: (v) => { stateProxy.currentStep = v; }
|
||||
},
|
||||
editingWorkId: {
|
||||
get: () => stateProxy.editingWorkId,
|
||||
set: (v) => { stateProxy.editingWorkId = v; }
|
||||
},
|
||||
workEntryCounter: {
|
||||
get: () => stateProxy.workEntryCounter,
|
||||
set: (v) => { stateProxy.workEntryCounter = v; }
|
||||
},
|
||||
currentDefectIndex: {
|
||||
get: () => stateProxy.currentDefectIndex,
|
||||
set: (v) => { stateProxy.currentDefectIndex = v; }
|
||||
},
|
||||
currentEditingField: {
|
||||
get: () => stateProxy.currentEditingField,
|
||||
set: (v) => { stateProxy.currentEditingField = v; }
|
||||
},
|
||||
currentTimeValue: {
|
||||
get: () => stateProxy.currentTimeValue,
|
||||
set: (v) => { stateProxy.currentTimeValue = v; }
|
||||
},
|
||||
selectedWorkplace: {
|
||||
get: () => stateProxy.selectedWorkplace,
|
||||
set: (v) => { stateProxy.selectedWorkplace = v; }
|
||||
},
|
||||
selectedWorkplaceName: {
|
||||
get: () => stateProxy.selectedWorkplaceName,
|
||||
set: (v) => { stateProxy.selectedWorkplaceName = v; }
|
||||
},
|
||||
selectedWorkplaceCategory: {
|
||||
get: () => stateProxy.selectedWorkplaceCategory,
|
||||
set: (v) => { stateProxy.selectedWorkplaceCategory = v; }
|
||||
},
|
||||
selectedWorkplaceCategoryName: {
|
||||
get: () => stateProxy.selectedWorkplaceCategoryName,
|
||||
set: (v) => { stateProxy.selectedWorkplaceCategoryName = v; }
|
||||
}
|
||||
});
|
||||
@@ -1,299 +0,0 @@
|
||||
/**
|
||||
* Daily Work Report - Utilities
|
||||
* 작업보고서 관련 유틸리티 함수들 (공통 함수는 CommonUtils에 위임)
|
||||
*/
|
||||
|
||||
class DailyWorkReportUtils {
|
||||
constructor() {
|
||||
this._common = window.CommonUtils;
|
||||
console.log('[Utils] DailyWorkReportUtils 초기화');
|
||||
}
|
||||
|
||||
// --- 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); }
|
||||
|
||||
// --- 작업보고 전용 ---
|
||||
|
||||
/**
|
||||
* 시간 포맷팅 (HH:mm)
|
||||
*/
|
||||
formatTime(time) {
|
||||
if (!time) return '-';
|
||||
if (typeof time === 'string' && time.includes(':')) {
|
||||
return time.substring(0, 5);
|
||||
}
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 라벨 반환
|
||||
*/
|
||||
getStatusLabel(status) {
|
||||
const labels = {
|
||||
'pending': '접수',
|
||||
'in_progress': '처리중',
|
||||
'resolved': '해결',
|
||||
'completed': '완료',
|
||||
'closed': '종료'
|
||||
};
|
||||
return labels[status] || status || '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* 숫자 포맷팅 (천 단위 콤마)
|
||||
*/
|
||||
formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toLocaleString('ko-KR');
|
||||
}
|
||||
|
||||
/**
|
||||
* 소수점 자리수 포맷팅
|
||||
*/
|
||||
formatDecimal(num, decimals = 1) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return Number(num).toFixed(decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 날짜 사이 일수 계산
|
||||
*/
|
||||
daysBetween(date1, date2) {
|
||||
const d1 = new Date(date1);
|
||||
const d2 = new Date(date2);
|
||||
const diffTime = Math.abs(d2 - d1);
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
/**
|
||||
* 숫자 유효성 검사
|
||||
*/
|
||||
isValidNumber(value) {
|
||||
return !isNaN(value) && isFinite(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 시간 유효성 검사 (0-24)
|
||||
*/
|
||||
isValidHours(hours) {
|
||||
const num = parseFloat(hours);
|
||||
return this.isValidNumber(num) && num >= 0 && num <= 24;
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿼리 스트링 파싱
|
||||
*/
|
||||
parseQueryString(queryString) {
|
||||
const params = new URLSearchParams(queryString);
|
||||
const result = {};
|
||||
for (const [key, value] of params) {
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿼리 스트링 생성
|
||||
*/
|
||||
buildQueryString(params) {
|
||||
return new URLSearchParams(params).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 로컬 스토리지 안전하게 가져오기
|
||||
*/
|
||||
getLocalStorage(key, defaultValue = null) {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
} catch (error) {
|
||||
console.error('[Utils] localStorage 읽기 오류:', error);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 로컬 스토리지 안전하게 저장하기
|
||||
*/
|
||||
setLocalStorage(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Utils] localStorage 저장 오류:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열 정렬 (다중 키)
|
||||
*/
|
||||
sortBy(array, ...keys) {
|
||||
return [...array].sort((a, b) => {
|
||||
for (const key of keys) {
|
||||
const direction = key.startsWith('-') ? -1 : 1;
|
||||
const actualKey = key.replace(/^-/, '');
|
||||
const aVal = a[actualKey];
|
||||
const bVal = b[actualKey];
|
||||
|
||||
if (aVal < bVal) return -1 * direction;
|
||||
if (aVal > bVal) return 1 * direction;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.DailyWorkReportUtils = new DailyWorkReportUtils();
|
||||
|
||||
// 하위 호환성: 기존 함수들
|
||||
window.getKoreaToday = () => window.DailyWorkReportUtils.getKoreaToday();
|
||||
window.formatDateForApi = (date) => window.DailyWorkReportUtils.formatDateForApi(date);
|
||||
window.formatDate = (date) => window.DailyWorkReportUtils.formatDate(date);
|
||||
window.getStatusLabel = (status) => window.DailyWorkReportUtils.getStatusLabel(status);
|
||||
|
||||
// 메시지 표시 함수들
|
||||
window.showMessage = function(message, type = 'info') {
|
||||
const container = document.getElementById('message-container');
|
||||
if (!container) {
|
||||
console.log(`[Message] ${type}: ${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `<div class="message ${type}">${message}</div>`;
|
||||
|
||||
if (type === 'success') {
|
||||
setTimeout(() => window.hideMessage(), 5000);
|
||||
}
|
||||
};
|
||||
|
||||
window.hideMessage = function() {
|
||||
const container = document.getElementById('message-container');
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 저장 결과 모달
|
||||
window.showSaveResultModal = function(type, title, message, details = null) {
|
||||
const modal = document.getElementById('saveResultModal');
|
||||
const titleElement = document.getElementById('resultModalTitle');
|
||||
const contentElement = document.getElementById('resultModalContent');
|
||||
|
||||
if (!modal || !contentElement) {
|
||||
alert(`${title}\n\n${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const icons = {
|
||||
success: '✅',
|
||||
error: '❌',
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️'
|
||||
};
|
||||
|
||||
let content = `
|
||||
<div class="result-icon ${type}">${icons[type] || icons.info}</div>
|
||||
<h3 class="result-title ${type}">${title}</h3>
|
||||
<p class="result-message">${message}</p>
|
||||
`;
|
||||
|
||||
if (details) {
|
||||
if (Array.isArray(details) && details.length > 0) {
|
||||
content += `
|
||||
<div class="result-details">
|
||||
<h4>상세 정보:</h4>
|
||||
<ul>${details.map(d => `<li>${d}</li>`).join('')}</ul>
|
||||
</div>
|
||||
`;
|
||||
} else if (typeof details === 'string') {
|
||||
content += `<div class="result-details"><p>${details}</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (titleElement) titleElement.textContent = '저장 결과';
|
||||
contentElement.innerHTML = content;
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// ESC 키로 닫기
|
||||
const escHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
window.closeSaveResultModal();
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
|
||||
// 배경 클릭으로 닫기
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) {
|
||||
window.closeSaveResultModal();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
window.closeSaveResultModal = function() {
|
||||
const modal = document.getElementById('saveResultModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
// 단계 이동 함수
|
||||
window.goToStep = function(stepNumber) {
|
||||
const state = window.DailyWorkReportState;
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const step = document.getElementById(`step${i}`);
|
||||
if (step) {
|
||||
step.classList.remove('active', 'completed');
|
||||
if (i < stepNumber) {
|
||||
step.classList.add('completed');
|
||||
const stepNum = step.querySelector('.step-number');
|
||||
if (stepNum) stepNum.classList.add('completed');
|
||||
} else if (i === stepNumber) {
|
||||
step.classList.add('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.updateProgressSteps(stepNumber);
|
||||
state.currentStep = stepNumber;
|
||||
};
|
||||
|
||||
window.updateProgressSteps = function(currentStepNumber) {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const progressStep = document.getElementById(`progressStep${i}`);
|
||||
if (progressStep) {
|
||||
progressStep.classList.remove('active', 'completed');
|
||||
if (i < currentStepNumber) {
|
||||
progressStep.classList.add('completed');
|
||||
} else if (i === currentStepNumber) {
|
||||
progressStep.classList.add('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// showToast → api-base.js 전역 사용
|
||||
|
||||
// 확인 다이얼로그
|
||||
window.showConfirmDialog = function(message, onConfirm, onCancel) {
|
||||
if (confirm(message)) {
|
||||
onConfirm?.();
|
||||
} else {
|
||||
onCancel?.();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user