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:
624
system1-factory/web/public/js/tbm/api.js
Normal file
624
system1-factory/web/public/js/tbm/api.js
Normal file
@@ -0,0 +1,624 @@
|
||||
/**
|
||||
* TBM - API Client
|
||||
* TBM 관련 모든 API 호출을 관리
|
||||
*/
|
||||
|
||||
class TbmAPI {
|
||||
constructor() {
|
||||
this.state = window.TbmState;
|
||||
this.utils = window.TbmUtils;
|
||||
console.log('[TbmAPI] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 초기 데이터 로드 (작업자, 프로젝트, 안전 체크리스트, 공정, 작업, 작업장)
|
||||
*/
|
||||
async loadInitialData() {
|
||||
try {
|
||||
// 현재 로그인한 사용자 정보 가져오기
|
||||
const userInfo = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
||||
this.state.currentUser = userInfo;
|
||||
|
||||
// 병렬로 데이터 로드
|
||||
await Promise.all([
|
||||
this.loadWorkers(),
|
||||
this.loadProjects(),
|
||||
this.loadSafetyChecks(),
|
||||
this.loadWorkTypes(),
|
||||
this.loadTasks(),
|
||||
this.loadWorkplaces(),
|
||||
this.loadWorkplaceCategories()
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
console.error(' 초기 데이터 로드 오류:', error);
|
||||
window.showToast?.('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업자 목록 로드 (생산팀 소속만)
|
||||
*/
|
||||
async loadWorkers() {
|
||||
try {
|
||||
const response = await window.apiCall('/workers?limit=1000&department_id=1');
|
||||
if (response) {
|
||||
let workers = Array.isArray(response) ? response : (response.data || []);
|
||||
// 활성 상태인 작업자만 필터링
|
||||
workers = workers.filter(w => w.status === 'active' && w.employment_status === 'employed');
|
||||
this.state.allWorkers = workers;
|
||||
return workers;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(' 작업자 로딩 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로젝트 목록 로드 (활성 프로젝트만)
|
||||
*/
|
||||
async loadProjects() {
|
||||
try {
|
||||
const response = await window.apiCall('/projects?is_active=1');
|
||||
if (response) {
|
||||
const projects = Array.isArray(response) ? response : (response.data || []);
|
||||
this.state.allProjects = projects.filter(p =>
|
||||
p.is_active === 1 || p.is_active === true || p.is_active === '1'
|
||||
);
|
||||
return this.state.allProjects;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(' 프로젝트 로딩 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 안전 체크리스트 로드
|
||||
*/
|
||||
async loadSafetyChecks() {
|
||||
try {
|
||||
const response = await window.apiCall('/tbm/safety-checks');
|
||||
if (response && response.success) {
|
||||
this.state.allSafetyChecks = response.data;
|
||||
return this.state.allSafetyChecks;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(' 안전 체크리스트 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정(Work Types) 목록 로드
|
||||
*/
|
||||
async loadWorkTypes() {
|
||||
try {
|
||||
const response = await window.apiCall('/daily-work-reports/work-types');
|
||||
if (response && response.success) {
|
||||
this.state.allWorkTypes = response.data || [];
|
||||
return this.state.allWorkTypes;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(' 공정 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업(Tasks) 목록 로드
|
||||
*/
|
||||
async loadTasks() {
|
||||
try {
|
||||
const response = await window.apiCall('/tasks/active/list');
|
||||
if (response && response.success) {
|
||||
this.state.allTasks = response.data || [];
|
||||
return this.state.allTasks;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(' 작업 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 목록 로드
|
||||
*/
|
||||
async loadWorkplaces() {
|
||||
try {
|
||||
const response = await window.apiCall('/workplaces?is_active=true');
|
||||
if (response && response.success) {
|
||||
this.state.allWorkplaces = response.data || [];
|
||||
return this.state.allWorkplaces;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(' 작업장 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 카테고리 로드
|
||||
*/
|
||||
async loadWorkplaceCategories() {
|
||||
try {
|
||||
const response = await window.apiCall('/workplaces/categories/active/list');
|
||||
if (response && response.success) {
|
||||
this.state.allWorkplaceCategories = response.data || [];
|
||||
return this.state.allWorkplaceCategories;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(' 작업장 카테고리 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 오늘의 TBM만 로드 (TBM 입력 탭용)
|
||||
*/
|
||||
async loadTodayOnlyTbm() {
|
||||
const today = this.utils.getTodayKST();
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/date/${today}`);
|
||||
|
||||
if (response && response.success) {
|
||||
this.state.todaySessions = response.data || [];
|
||||
} else {
|
||||
this.state.todaySessions = [];
|
||||
}
|
||||
return this.state.todaySessions;
|
||||
} catch (error) {
|
||||
console.error(' 오늘 TBM 조회 오류:', error);
|
||||
window.showToast?.('오늘 TBM을 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
this.state.todaySessions = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 최근 TBM을 날짜별로 그룹화하여 로드
|
||||
*/
|
||||
async loadRecentTbmGroupedByDate() {
|
||||
try {
|
||||
const today = new Date();
|
||||
const dates = [];
|
||||
|
||||
// 최근 N일의 날짜 생성
|
||||
for (let i = 0; i < this.state.loadedDaysCount; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
dates.push(dateStr);
|
||||
}
|
||||
|
||||
// 각 날짜의 TBM 로드
|
||||
this.state.dateGroupedSessions = {};
|
||||
this.state.allLoadedSessions = [];
|
||||
|
||||
const promises = dates.map(date => window.apiCall(`/tbm/sessions/date/${date}`));
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
results.forEach((response, index) => {
|
||||
const date = dates[index];
|
||||
if (response && response.success && response.data && response.data.length > 0) {
|
||||
const sessions = response.data;
|
||||
|
||||
if (sessions.length > 0) {
|
||||
this.state.dateGroupedSessions[date] = sessions;
|
||||
this.state.allLoadedSessions = this.state.allLoadedSessions.concat(sessions);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return this.state.dateGroupedSessions;
|
||||
|
||||
} catch (error) {
|
||||
console.error(' TBM 날짜별 로드 오류:', error);
|
||||
window.showToast?.('TBM을 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
this.state.dateGroupedSessions = {};
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 날짜의 TBM 세션 목록 로드
|
||||
*/
|
||||
async loadTbmSessionsByDate(date) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/date/${date}`);
|
||||
|
||||
if (response && response.success) {
|
||||
this.state.allSessions = response.data || [];
|
||||
} else {
|
||||
this.state.allSessions = [];
|
||||
}
|
||||
return this.state.allSessions;
|
||||
} catch (error) {
|
||||
console.error(' TBM 세션 조회 오류:', error);
|
||||
window.showToast?.('TBM 세션을 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
this.state.allSessions = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 세션 생성
|
||||
*/
|
||||
async createTbmSession(sessionData) {
|
||||
try {
|
||||
const response = await window.apiCall('/tbm/sessions', 'POST', sessionData);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '세션 생성 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' TBM 세션 생성 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 세션 정보 조회
|
||||
*/
|
||||
async getSession(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}`);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '세션 조회 실패');
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(' TBM 세션 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 팀원 조회
|
||||
*/
|
||||
async getTeamMembers(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/team`);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '팀원 조회 실패');
|
||||
}
|
||||
return response.data || [];
|
||||
} catch (error) {
|
||||
console.error(' TBM 팀원 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 팀원 일괄 추가
|
||||
*/
|
||||
async addTeamMembers(sessionId, members) {
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/tbm/sessions/${sessionId}/team/batch`,
|
||||
'POST',
|
||||
{ members }
|
||||
);
|
||||
if (!response || !response.success) {
|
||||
const err = new Error(response?.message || '팀원 추가 실패');
|
||||
if (response && response.duplicates) err.duplicates = response.duplicates;
|
||||
throw err;
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' TBM 팀원 추가 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 팀원 전체 삭제
|
||||
*/
|
||||
async clearTeamMembers(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/team/clear`, 'DELETE');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' TBM 팀원 삭제 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 안전 체크 조회
|
||||
*/
|
||||
async getSafetyChecks(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/safety`);
|
||||
return response?.data || [];
|
||||
} catch (error) {
|
||||
console.error(' 안전 체크 조회 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 안전 체크 (필터링된) 조회
|
||||
*/
|
||||
async getFilteredSafetyChecks(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}/safety-checks/filtered`);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '체크리스트를 불러올 수 없습니다.');
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(' 필터링된 안전 체크 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 안전 체크 저장
|
||||
*/
|
||||
async saveSafetyChecks(sessionId, records) {
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/tbm/sessions/${sessionId}/safety`,
|
||||
'POST',
|
||||
{ records }
|
||||
);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '저장 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' 안전 체크 저장 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 세션 완료 처리
|
||||
*/
|
||||
async completeTbmSession(sessionId, endTime) {
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/tbm/sessions/${sessionId}/complete`,
|
||||
'POST',
|
||||
{ end_time: endTime }
|
||||
);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '완료 처리 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' TBM 완료 처리 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 인계 저장
|
||||
*/
|
||||
async saveHandover(handoverData) {
|
||||
try {
|
||||
const response = await window.apiCall('/tbm/handovers', 'POST', handoverData);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '인계 요청 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' 작업 인계 저장 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리별 작업장 로드
|
||||
*/
|
||||
async loadWorkplacesByCategory(categoryId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/workplaces?category_id=${categoryId}`);
|
||||
if (!response || !response.success || !response.data) {
|
||||
return [];
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(' 작업장 로드 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 지도 영역 로드
|
||||
*/
|
||||
async loadMapRegions(categoryId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/workplaces/categories/${categoryId}/map-regions`);
|
||||
if (response && response.success) {
|
||||
this.state.mapRegions = response.data || [];
|
||||
return this.state.mapRegions;
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error(' 지도 영역 로드 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 세션 삭제
|
||||
*/
|
||||
async deleteSession(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}`, 'DELETE');
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '삭제 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' TBM 세션 삭제 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 팀원 분할 배정
|
||||
*/
|
||||
async splitAssignment(sessionId, splitData) {
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/tbm/sessions/${sessionId}/team/split`,
|
||||
'POST',
|
||||
splitData
|
||||
);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '분할 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' 분할 배정 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 팀원 단일 추가/수정 (POST /team)
|
||||
*/
|
||||
async updateTeamMember(sessionId, memberData) {
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/tbm/sessions/${sessionId}/team`,
|
||||
'POST',
|
||||
memberData
|
||||
);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '팀원 수정 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' 팀원 수정 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 인원 이동 (분할→이동 / 빼오기)
|
||||
*/
|
||||
async transfer(transferData) {
|
||||
try {
|
||||
const response = await window.apiCall('/tbm/transfers', 'POST', transferData);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '이동 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' TBM 이동 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 생성
|
||||
*/
|
||||
async createTask(taskData) {
|
||||
try {
|
||||
const response = await window.apiCall('/tasks', 'POST', taskData);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '작업 생성 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' 작업 생성 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 작업장 전체 목록 (active/list)
|
||||
*/
|
||||
async loadActiveWorkplacesList() {
|
||||
try {
|
||||
const response = await window.apiCall('/workplaces/active/list');
|
||||
if (response && response.success) {
|
||||
return response.data || [];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error(' 활성 작업장 목록 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 당일 배정 현황 조회
|
||||
*/
|
||||
async loadTodayAssignments(date) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/date/${date}/assignments`);
|
||||
if (response && response.success) {
|
||||
return response.data || [];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error(' 배정 현황 조회 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 날짜의 TBM 세션 조회 (raw - 상태 변경 없음)
|
||||
*/
|
||||
async fetchSessionsByDate(date) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/date/${date}`);
|
||||
if (response && response.success) {
|
||||
return response.data || [];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error(' TBM 세션 조회 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 세션 완료 처리 (근태 정보 포함)
|
||||
*/
|
||||
async completeTbmWithAttendance(sessionId, attendanceData) {
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/tbm/sessions/${sessionId}/complete`,
|
||||
'POST',
|
||||
{ attendance: attendanceData }
|
||||
);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '완료 처리 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(' TBM 완료 처리 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.TbmAPI = new TbmAPI();
|
||||
|
||||
// 하위 호환성: 기존 함수들
|
||||
window.loadInitialData = () => window.TbmAPI.loadInitialData();
|
||||
window.loadTodayOnlyTbm = () => window.TbmAPI.loadTodayOnlyTbm();
|
||||
window.loadTodayTbm = () => window.TbmAPI.loadRecentTbmGroupedByDate();
|
||||
window.loadAllTbm = () => {
|
||||
window.TbmState.loadedDaysCount = 30;
|
||||
return window.TbmAPI.loadRecentTbmGroupedByDate();
|
||||
};
|
||||
window.loadRecentTbmGroupedByDate = () => window.TbmAPI.loadRecentTbmGroupedByDate();
|
||||
window.loadTbmSessionsByDate = (date) => window.TbmAPI.loadTbmSessionsByDate(date);
|
||||
window.loadWorkplaceCategories = () => window.TbmAPI.loadWorkplaceCategories();
|
||||
window.loadWorkplacesByCategory = (categoryId) => window.TbmAPI.loadWorkplacesByCategory(categoryId);
|
||||
|
||||
// 더 많은 날짜 로드
|
||||
window.loadMoreTbmDays = async function() {
|
||||
window.TbmState.loadedDaysCount += 7;
|
||||
await window.TbmAPI.loadRecentTbmGroupedByDate();
|
||||
window.showToast?.(`최근 ${window.TbmState.loadedDaysCount}일의 TBM을 로드했습니다.`, 'success');
|
||||
};
|
||||
window.deleteTbmSession = (sessionId) => window.TbmAPI.deleteSession(sessionId);
|
||||
window.fetchSessionsByDate = (date) => window.TbmAPI.fetchSessionsByDate(date);
|
||||
|
||||
console.log('[Module] tbm/api.js 로드 완료');
|
||||
322
system1-factory/web/public/js/tbm/state.js
Normal file
322
system1-factory/web/public/js/tbm/state.js
Normal file
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* TBM - State Manager
|
||||
* TBM 페이지의 전역 상태 관리 (BaseState 상속)
|
||||
*/
|
||||
|
||||
class TbmState extends BaseState {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// 세션 데이터
|
||||
this.allSessions = [];
|
||||
this.todaySessions = [];
|
||||
this.dateGroupedSessions = {};
|
||||
this.allLoadedSessions = [];
|
||||
this.loadedDaysCount = 7;
|
||||
|
||||
// 마스터 데이터
|
||||
this.allWorkers = [];
|
||||
this.allProjects = [];
|
||||
this.allWorkTypes = [];
|
||||
this.allTasks = [];
|
||||
this.allSafetyChecks = [];
|
||||
this.allWorkplaces = [];
|
||||
this.allWorkplaceCategories = [];
|
||||
|
||||
// 현재 상태
|
||||
this.currentUser = null;
|
||||
this.currentSessionId = null;
|
||||
this.currentTab = 'tbm-input';
|
||||
|
||||
// 작업자 관련
|
||||
this.selectedWorkers = new Set();
|
||||
this.workerTaskList = [];
|
||||
this.selectedWorkersInModal = new Set();
|
||||
this.currentEditingTaskLine = null;
|
||||
|
||||
// 작업장 선택 관련
|
||||
this.selectedCategory = null;
|
||||
this.selectedWorkplace = null;
|
||||
this.selectedCategoryName = '';
|
||||
this.selectedWorkplaceName = '';
|
||||
|
||||
// 일괄 설정 관련
|
||||
this.isBulkMode = false;
|
||||
this.bulkSelectedWorkers = new Set();
|
||||
|
||||
// 지도 관련
|
||||
this.mapCanvas = null;
|
||||
this.mapCtx = null;
|
||||
this.mapImage = null;
|
||||
this.mapRegions = [];
|
||||
|
||||
console.log('[TbmState] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin 여부 확인
|
||||
*/
|
||||
isAdminUser() {
|
||||
const user = this.getUser();
|
||||
if (!user) return false;
|
||||
const role = (user.role || '').toLowerCase();
|
||||
return role === 'admin' || role === 'system admin' || role === 'system';
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 변경
|
||||
*/
|
||||
setCurrentTab(tab) {
|
||||
const prevTab = this.currentTab;
|
||||
this.currentTab = tab;
|
||||
this.notifyListeners('currentTab', tab, prevTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업자 목록에 추가
|
||||
*/
|
||||
addWorkerToList(worker) {
|
||||
this.workerTaskList.push({
|
||||
user_id: worker.user_id,
|
||||
worker_name: worker.worker_name,
|
||||
job_type: worker.job_type,
|
||||
tasks: [this.createEmptyTaskLine()]
|
||||
});
|
||||
this.notifyListeners('workerTaskList', this.workerTaskList, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 빈 작업 라인 생성
|
||||
*/
|
||||
createEmptyTaskLine() {
|
||||
return {
|
||||
task_line_id: window.CommonUtils.generateUUID(),
|
||||
project_id: null,
|
||||
work_type_id: null,
|
||||
task_id: null,
|
||||
workplace_category_id: null,
|
||||
workplace_id: null,
|
||||
workplace_category_name: '',
|
||||
workplace_name: '',
|
||||
work_detail: null,
|
||||
is_present: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업자에 작업 라인 추가
|
||||
*/
|
||||
addTaskLineToWorker(workerIndex) {
|
||||
if (this.workerTaskList[workerIndex]) {
|
||||
this.workerTaskList[workerIndex].tasks.push(this.createEmptyTaskLine());
|
||||
this.notifyListeners('workerTaskList', this.workerTaskList, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 라인 제거
|
||||
*/
|
||||
removeTaskLine(workerIndex, taskIndex) {
|
||||
if (this.workerTaskList[workerIndex]?.tasks) {
|
||||
this.workerTaskList[workerIndex].tasks.splice(taskIndex, 1);
|
||||
this.notifyListeners('workerTaskList', this.workerTaskList, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업자 제거
|
||||
*/
|
||||
removeWorkerFromList(workerIndex) {
|
||||
const removed = this.workerTaskList.splice(workerIndex, 1);
|
||||
this.notifyListeners('workerTaskList', this.workerTaskList, null);
|
||||
return removed[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 선택 초기화
|
||||
*/
|
||||
resetWorkplaceSelection() {
|
||||
this.selectedCategory = null;
|
||||
this.selectedWorkplace = null;
|
||||
this.selectedCategoryName = '';
|
||||
this.selectedWorkplaceName = '';
|
||||
this.mapCanvas = null;
|
||||
this.mapCtx = null;
|
||||
this.mapImage = null;
|
||||
this.mapRegions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 설정 초기화
|
||||
*/
|
||||
resetBulkSettings() {
|
||||
this.isBulkMode = false;
|
||||
this.bulkSelectedWorkers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜별 세션 그룹화
|
||||
*/
|
||||
groupSessionsByDate(sessions) {
|
||||
this.dateGroupedSessions = {};
|
||||
this.allLoadedSessions = [];
|
||||
|
||||
sessions.forEach(session => {
|
||||
const date = window.CommonUtils.formatDate(session.session_date);
|
||||
if (!this.dateGroupedSessions[date]) {
|
||||
this.dateGroupedSessions[date] = [];
|
||||
}
|
||||
this.dateGroupedSessions[date].push(session);
|
||||
this.allLoadedSessions.push(session);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 초기화
|
||||
*/
|
||||
reset() {
|
||||
this.workerTaskList = [];
|
||||
this.selectedWorkers.clear();
|
||||
this.selectedWorkersInModal.clear();
|
||||
this.currentEditingTaskLine = null;
|
||||
this.resetWorkplaceSelection();
|
||||
this.resetBulkSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* 디버그 출력
|
||||
*/
|
||||
debug() {
|
||||
console.log('[TbmState] 현재 상태:', {
|
||||
allSessions: this.allSessions.length,
|
||||
todaySessions: this.todaySessions.length,
|
||||
allWorkers: this.allWorkers.length,
|
||||
allProjects: this.allProjects.length,
|
||||
workerTaskList: this.workerTaskList.length,
|
||||
currentTab: this.currentTab
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.TbmState = new TbmState();
|
||||
|
||||
// 하위 호환성을 위한 전역 변수 프록시
|
||||
const tbmStateProxy = window.TbmState;
|
||||
|
||||
Object.defineProperties(window, {
|
||||
allSessions: {
|
||||
get: () => tbmStateProxy.allSessions,
|
||||
set: (v) => { tbmStateProxy.allSessions = v; }
|
||||
},
|
||||
todaySessions: {
|
||||
get: () => tbmStateProxy.todaySessions,
|
||||
set: (v) => { tbmStateProxy.todaySessions = v; }
|
||||
},
|
||||
allWorkers: {
|
||||
get: () => tbmStateProxy.allWorkers,
|
||||
set: (v) => { tbmStateProxy.allWorkers = v; }
|
||||
},
|
||||
allProjects: {
|
||||
get: () => tbmStateProxy.allProjects,
|
||||
set: (v) => { tbmStateProxy.allProjects = v; }
|
||||
},
|
||||
allWorkTypes: {
|
||||
get: () => tbmStateProxy.allWorkTypes,
|
||||
set: (v) => { tbmStateProxy.allWorkTypes = v; }
|
||||
},
|
||||
allTasks: {
|
||||
get: () => tbmStateProxy.allTasks,
|
||||
set: (v) => { tbmStateProxy.allTasks = v; }
|
||||
},
|
||||
allSafetyChecks: {
|
||||
get: () => tbmStateProxy.allSafetyChecks,
|
||||
set: (v) => { tbmStateProxy.allSafetyChecks = v; }
|
||||
},
|
||||
allWorkplaces: {
|
||||
get: () => tbmStateProxy.allWorkplaces,
|
||||
set: (v) => { tbmStateProxy.allWorkplaces = v; }
|
||||
},
|
||||
allWorkplaceCategories: {
|
||||
get: () => tbmStateProxy.allWorkplaceCategories,
|
||||
set: (v) => { tbmStateProxy.allWorkplaceCategories = v; }
|
||||
},
|
||||
currentUser: {
|
||||
get: () => tbmStateProxy.currentUser,
|
||||
set: (v) => { tbmStateProxy.currentUser = v; }
|
||||
},
|
||||
currentSessionId: {
|
||||
get: () => tbmStateProxy.currentSessionId,
|
||||
set: (v) => { tbmStateProxy.currentSessionId = v; }
|
||||
},
|
||||
selectedWorkers: {
|
||||
get: () => tbmStateProxy.selectedWorkers,
|
||||
set: (v) => { tbmStateProxy.selectedWorkers = v; }
|
||||
},
|
||||
workerTaskList: {
|
||||
get: () => tbmStateProxy.workerTaskList,
|
||||
set: (v) => { tbmStateProxy.workerTaskList = v; }
|
||||
},
|
||||
selectedWorkersInModal: {
|
||||
get: () => tbmStateProxy.selectedWorkersInModal,
|
||||
set: (v) => { tbmStateProxy.selectedWorkersInModal = v; }
|
||||
},
|
||||
currentEditingTaskLine: {
|
||||
get: () => tbmStateProxy.currentEditingTaskLine,
|
||||
set: (v) => { tbmStateProxy.currentEditingTaskLine = v; }
|
||||
},
|
||||
selectedCategory: {
|
||||
get: () => tbmStateProxy.selectedCategory,
|
||||
set: (v) => { tbmStateProxy.selectedCategory = v; }
|
||||
},
|
||||
selectedWorkplace: {
|
||||
get: () => tbmStateProxy.selectedWorkplace,
|
||||
set: (v) => { tbmStateProxy.selectedWorkplace = v; }
|
||||
},
|
||||
selectedCategoryName: {
|
||||
get: () => tbmStateProxy.selectedCategoryName,
|
||||
set: (v) => { tbmStateProxy.selectedCategoryName = v; }
|
||||
},
|
||||
selectedWorkplaceName: {
|
||||
get: () => tbmStateProxy.selectedWorkplaceName,
|
||||
set: (v) => { tbmStateProxy.selectedWorkplaceName = v; }
|
||||
},
|
||||
isBulkMode: {
|
||||
get: () => tbmStateProxy.isBulkMode,
|
||||
set: (v) => { tbmStateProxy.isBulkMode = v; }
|
||||
},
|
||||
bulkSelectedWorkers: {
|
||||
get: () => tbmStateProxy.bulkSelectedWorkers,
|
||||
set: (v) => { tbmStateProxy.bulkSelectedWorkers = v; }
|
||||
},
|
||||
dateGroupedSessions: {
|
||||
get: () => tbmStateProxy.dateGroupedSessions,
|
||||
set: (v) => { tbmStateProxy.dateGroupedSessions = v; }
|
||||
},
|
||||
allLoadedSessions: {
|
||||
get: () => tbmStateProxy.allLoadedSessions,
|
||||
set: (v) => { tbmStateProxy.allLoadedSessions = v; }
|
||||
},
|
||||
loadedDaysCount: {
|
||||
get: () => tbmStateProxy.loadedDaysCount,
|
||||
set: (v) => { tbmStateProxy.loadedDaysCount = v; }
|
||||
},
|
||||
mapRegions: {
|
||||
get: () => tbmStateProxy.mapRegions,
|
||||
set: (v) => { tbmStateProxy.mapRegions = v; }
|
||||
},
|
||||
mapCanvas: {
|
||||
get: () => tbmStateProxy.mapCanvas,
|
||||
set: (v) => { tbmStateProxy.mapCanvas = v; }
|
||||
},
|
||||
mapCtx: {
|
||||
get: () => tbmStateProxy.mapCtx,
|
||||
set: (v) => { tbmStateProxy.mapCtx = v; }
|
||||
},
|
||||
mapImage: {
|
||||
get: () => tbmStateProxy.mapImage,
|
||||
set: (v) => { tbmStateProxy.mapImage = v; }
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Module] tbm/state.js 로드 완료');
|
||||
127
system1-factory/web/public/js/tbm/utils.js
Normal file
127
system1-factory/web/public/js/tbm/utils.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* TBM - Utilities
|
||||
* TBM 관련 유틸리티 함수들 (공통 함수는 CommonUtils에 위임)
|
||||
*/
|
||||
|
||||
class TbmUtils {
|
||||
constructor() {
|
||||
this._common = window.CommonUtils;
|
||||
console.log('[TbmUtils] 초기화 완료');
|
||||
}
|
||||
|
||||
// --- 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); }
|
||||
|
||||
// --- TBM 전용 ---
|
||||
|
||||
/**
|
||||
* 날짜 표시용 포맷 (MM월 DD일)
|
||||
*/
|
||||
formatDateDisplay(dateString) {
|
||||
if (!dateString) return '';
|
||||
const [year, month, day] = dateString.split('-');
|
||||
return `${parseInt(month)}월 ${parseInt(day)}일`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜를 연/월/일/요일 형식으로 포맷
|
||||
*/
|
||||
formatDateFull(dateString) {
|
||||
if (!dateString) return '';
|
||||
const [year, month, day] = dateString.split('-');
|
||||
const dayName = this._common.getDayOfWeek(dateString);
|
||||
return `${year}년 ${parseInt(month)}월 ${parseInt(day)}일 (${dayName})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 시간을 HH:MM 형식으로 반환
|
||||
*/
|
||||
getCurrentTime() {
|
||||
return new Date().toTimeString().slice(0, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨 조건명 반환
|
||||
*/
|
||||
getWeatherConditionName(code) {
|
||||
const names = {
|
||||
clear: '맑음', rain: '비', snow: '눈', heat: '폭염',
|
||||
cold: '한파', wind: '강풍', fog: '안개', dust: '미세먼지'
|
||||
};
|
||||
return names[code] || code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨 아이콘 반환
|
||||
*/
|
||||
getWeatherIcon(code) {
|
||||
const icons = {
|
||||
clear: '☀️', rain: '🌧️', snow: '❄️', heat: '🔥',
|
||||
cold: '🥶', wind: '💨', fog: '🌫️', dust: '😷'
|
||||
};
|
||||
return icons[code] || '🌤️';
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리명 반환
|
||||
*/
|
||||
getCategoryName(category) {
|
||||
const names = {
|
||||
'PPE': '개인 보호 장비', 'EQUIPMENT': '장비 점검',
|
||||
'ENVIRONMENT': '작업 환경', 'EMERGENCY': '비상 대응',
|
||||
'WEATHER': '날씨', 'TASK': '작업'
|
||||
};
|
||||
return names[category] || category;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 배지 HTML 반환
|
||||
*/
|
||||
getStatusBadge(status) {
|
||||
const badges = {
|
||||
'draft': '<span class="tbm-card-status draft">진행중</span>',
|
||||
'completed': '<span class="tbm-card-status completed">완료</span>',
|
||||
'cancelled': '<span class="tbm-card-status cancelled">취소</span>'
|
||||
};
|
||||
return badges[status] || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.TbmUtils = new TbmUtils();
|
||||
|
||||
// 하위 호환성: TBM 전용 유틸 (showToast, formatDate, waitForApi, generateUUID는 api-base.js 전역)
|
||||
window.getTodayKST = () => window.TbmUtils.getTodayKST();
|
||||
|
||||
// 카테고리별 그룹화
|
||||
window.groupChecksByCategory = function(checks) {
|
||||
return checks.reduce((acc, check) => {
|
||||
const category = check.check_category || 'OTHER';
|
||||
if (!acc[category]) acc[category] = [];
|
||||
acc[category].push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
// 작업별 그룹화
|
||||
window.groupChecksByTask = function(checks) {
|
||||
return checks.reduce((acc, check) => {
|
||||
const taskId = check.task_id || 0;
|
||||
const taskName = check.task_name || '기타 작업';
|
||||
if (!acc[taskId]) acc[taskId] = { name: taskName, items: [] };
|
||||
acc[taskId].items.push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
// Admin 사용자 확인
|
||||
window.isAdminUser = function() {
|
||||
return window.TbmState?.isAdminUser() || false;
|
||||
};
|
||||
|
||||
console.log('[Module] tbm/utils.js 로드 완료');
|
||||
Reference in New Issue
Block a user