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:
625
system1-factory/web/public/js/tbm-create.js
Normal file
625
system1-factory/web/public/js/tbm-create.js
Normal file
@@ -0,0 +1,625 @@
|
||||
/**
|
||||
* TBM 모바일 위자드 - tbm-create.js
|
||||
* 3단계 위자드로 TBM 세션을 생성하는 모바일 전용 페이지 로직
|
||||
* Step 1: 작업자 선택, Step 2: 프로젝트+공정 선택, Step 3: 확인
|
||||
* (작업/작업장은 생성 후 세부 편집 단계에서 입력)
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ==================== 위자드 상태 ====================
|
||||
const W = {
|
||||
step: 1,
|
||||
totalSteps: 3,
|
||||
sessionDate: null,
|
||||
leaderId: null,
|
||||
leaderName: '',
|
||||
workers: new Set(), // user_id Set
|
||||
workerNames: {}, // { user_id: worker_name }
|
||||
projectId: null,
|
||||
projectName: '',
|
||||
workTypeId: null,
|
||||
workTypeName: '',
|
||||
showAddWorkType: false,
|
||||
todayAssignments: null // 당일 배정 현황 캐시
|
||||
};
|
||||
|
||||
const esc = window.escapeHtml || function(s) { return s || ''; };
|
||||
|
||||
// ==================== 초기화 ====================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
try {
|
||||
// apiCall이 준비될 때까지 대기
|
||||
await waitForApi();
|
||||
|
||||
// 초기 데이터 로드
|
||||
await window.TbmAPI.loadInitialData();
|
||||
|
||||
// 기본 정보 자동 설정
|
||||
W.sessionDate = window.TbmUtils.getTodayKST();
|
||||
var user = window.TbmState.getUser();
|
||||
if (user) {
|
||||
var uid = user.user_id || user.id;
|
||||
if (uid) {
|
||||
var worker = window.TbmState.allWorkers.find(function(w) { return String(w.user_id) === String(uid); });
|
||||
if (worker) {
|
||||
W.leaderId = worker.user_id;
|
||||
W.leaderName = worker.worker_name;
|
||||
} else {
|
||||
W.leaderId = uid;
|
||||
W.leaderName = user.name || '';
|
||||
}
|
||||
} else {
|
||||
W.leaderName = user.name || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 로딩 해제
|
||||
document.getElementById('loadingOverlay').style.display = 'none';
|
||||
|
||||
// 첫 스텝 렌더링
|
||||
renderStep(1);
|
||||
updateIndicator();
|
||||
updateNav();
|
||||
} catch (error) {
|
||||
console.error('초기화 오류:', error);
|
||||
document.getElementById('loadingOverlay').style.display = 'none';
|
||||
showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// waitForApi → api-base.js 전역 사용
|
||||
|
||||
// ==================== 네비게이션 ====================
|
||||
|
||||
window.nextStep = function() {
|
||||
console.log('[TBM Create] nextStep called, current step:', W.step, 'workTypeId:', W.workTypeId);
|
||||
if (!validateStep(W.step)) return;
|
||||
if (W.step < W.totalSteps) {
|
||||
W.step++;
|
||||
renderStep(W.step);
|
||||
updateIndicator();
|
||||
updateNav();
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
window.prevStep = function() {
|
||||
console.log('[TBM Create] prevStep called, current step:', W.step);
|
||||
if (W.step > 1) {
|
||||
W.step--;
|
||||
renderStep(W.step);
|
||||
updateIndicator();
|
||||
updateNav();
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
window.goBack = function() {
|
||||
if (W.step > 1) {
|
||||
window.prevStep();
|
||||
} else {
|
||||
window.location.href = '/pages/work/tbm-mobile.html';
|
||||
}
|
||||
};
|
||||
|
||||
function updateIndicator() {
|
||||
var steps = document.querySelectorAll('#stepIndicator .step');
|
||||
var lines = document.querySelectorAll('#stepIndicator .step-line');
|
||||
steps.forEach(function(el, i) {
|
||||
el.classList.remove('active', 'completed');
|
||||
if (i + 1 === W.step) {
|
||||
el.classList.add('active');
|
||||
} else if (i + 1 < W.step) {
|
||||
el.classList.add('completed');
|
||||
}
|
||||
});
|
||||
lines.forEach(function(el, i) {
|
||||
el.style.background = (i + 1 < W.step) ? '#10b981' : '#e5e7eb';
|
||||
});
|
||||
}
|
||||
|
||||
// 네비게이션 버튼: 단일 핸들러 (DOM 교체 없이 상태 기반 분기)
|
||||
var _navAction = { prev: null, next: null };
|
||||
|
||||
function updateNav() {
|
||||
var prevBtn = document.getElementById('prevBtn');
|
||||
var nextBtn = document.getElementById('nextBtn');
|
||||
|
||||
if (W.step === 1) {
|
||||
prevBtn.style.visibility = 'hidden';
|
||||
_navAction.prev = null;
|
||||
} else {
|
||||
prevBtn.style.visibility = 'visible';
|
||||
_navAction.prev = window.prevStep;
|
||||
}
|
||||
|
||||
if (W.step === W.totalSteps) {
|
||||
nextBtn.className = 'nav-btn nav-btn-save';
|
||||
nextBtn.textContent = '저장';
|
||||
_navAction.next = saveWizard;
|
||||
} else {
|
||||
nextBtn.className = 'nav-btn nav-btn-next';
|
||||
nextBtn.innerHTML = '다음 →';
|
||||
_navAction.next = window.nextStep;
|
||||
}
|
||||
nextBtn.disabled = false;
|
||||
}
|
||||
|
||||
// 한번만 등록하는 이벤트 리스너
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('prevBtn').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
if (_navAction.prev) _navAction.prev();
|
||||
});
|
||||
document.getElementById('nextBtn').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
if (_navAction.next) _navAction.next();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== 유효성 검사 ====================
|
||||
|
||||
function validateStep(step) {
|
||||
switch (step) {
|
||||
case 1: // 작업자 선택
|
||||
if (W.workers.size === 0) {
|
||||
showToast('최소 1명의 작업자를 선택해주세요.', 'warning');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
case 2: // 프로젝트 + 공정
|
||||
if (!W.workTypeId) {
|
||||
showToast('공정을 선택해주세요.', 'warning');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 스텝 렌더링 ====================
|
||||
|
||||
function renderStep(step) {
|
||||
var container = document.getElementById('stepContainer');
|
||||
switch (step) {
|
||||
case 1: renderStepWorkers(container); break;
|
||||
case 2: renderStepProjectAndWorkType(container); break;
|
||||
case 3: renderStepConfirm(container); break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 1: 작업자 선택 ---
|
||||
async function renderStepWorkers(container) {
|
||||
var workers = window.TbmState.allWorkers;
|
||||
|
||||
// 당일 배정 현황 로드 (첫 로드 시)
|
||||
if (!W.todayAssignments) {
|
||||
try {
|
||||
var today = window.TbmUtils.getTodayKST();
|
||||
var res = await window.apiCall('/tbm/sessions/date/' + today + '/assignments');
|
||||
if (res && res.success) {
|
||||
W.todayAssignments = {};
|
||||
res.data.forEach(function(a) {
|
||||
if (a.sessions && a.sessions.length > 0) {
|
||||
W.todayAssignments[a.user_id] = a;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
W.todayAssignments = {};
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('배정 현황 로드 오류:', e);
|
||||
W.todayAssignments = {};
|
||||
}
|
||||
}
|
||||
|
||||
var workerCards = workers.map(function(w) {
|
||||
var selected = W.workers.has(w.user_id) ? ' selected' : '';
|
||||
var assignment = W.todayAssignments[w.user_id];
|
||||
var assigned = assignment && assignment.sessions && assignment.sessions.length > 0;
|
||||
|
||||
var badgeHtml = '';
|
||||
var disabledClass = '';
|
||||
var onclick = 'toggleWorker(' + w.user_id + ')';
|
||||
|
||||
if (assigned) {
|
||||
// 이미 배정됨 - 선택 불가
|
||||
var leaderNames = assignment.sessions.map(function(s) { return s.leader_name || ''; }).join(', ');
|
||||
badgeHtml = '<div style="font-size:0.625rem; color:#ef4444; margin-top:0.125rem;">배정됨 - ' + esc(leaderNames) + ' TBM</div>';
|
||||
disabledClass = ' disabled';
|
||||
onclick = '';
|
||||
}
|
||||
|
||||
return '<div class="worker-card' + selected + disabledClass + '"' +
|
||||
(onclick ? ' onclick="' + onclick + '"' : '') +
|
||||
' data-wid="' + w.user_id + '"' +
|
||||
' style="' + (assigned ? 'opacity:0.5; pointer-events:none;' : '') + '">' +
|
||||
'<div class="worker-check">✓</div>' +
|
||||
'<div class="worker-info">' +
|
||||
'<div class="worker-name">' + esc(w.worker_name) + '</div>' +
|
||||
'<div class="worker-type">' + esc(w.job_type || '작업자') + '</div>' +
|
||||
badgeHtml +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="wizard-section">' +
|
||||
'<div class="section-title"><span class="sn">1</span>작업자 선택</div>' +
|
||||
'<div class="select-all-bar">' +
|
||||
'<span class="count" id="workerCount">' + W.workers.size + '명 선택</span>' +
|
||||
'<button type="button" class="select-all-btn" onclick="toggleAllWorkers()">' +
|
||||
(W.workers.size === workers.length ? '전체 해제' : '전체 선택') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="worker-grid">' + workerCards + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
window.toggleWorker = function(workerId) {
|
||||
// 이미 배정된 작업자는 선택 불가
|
||||
var a = W.todayAssignments && W.todayAssignments[workerId];
|
||||
if (a && a.sessions && a.sessions.length > 0) return;
|
||||
|
||||
if (W.workers.has(workerId)) {
|
||||
W.workers.delete(workerId);
|
||||
delete W.workerNames[workerId];
|
||||
} else {
|
||||
W.workers.add(workerId);
|
||||
var w = window.TbmState.allWorkers.find(function(x) { return x.user_id === workerId; });
|
||||
if (w) W.workerNames[workerId] = w.worker_name;
|
||||
}
|
||||
var card = document.querySelector('[data-wid="' + workerId + '"]');
|
||||
if (card) card.classList.toggle('selected');
|
||||
var countEl = document.getElementById('workerCount');
|
||||
if (countEl) countEl.textContent = W.workers.size + '명 선택';
|
||||
};
|
||||
|
||||
window.toggleAllWorkers = function() {
|
||||
var workers = window.TbmState.allWorkers;
|
||||
var availableWorkers = workers.filter(function(w) {
|
||||
var a = W.todayAssignments && W.todayAssignments[w.user_id];
|
||||
return !(a && a.sessions && a.sessions.length > 0);
|
||||
});
|
||||
if (W.workers.size === availableWorkers.length) {
|
||||
W.workers.clear();
|
||||
W.workerNames = {};
|
||||
} else {
|
||||
availableWorkers.forEach(function(w) {
|
||||
W.workers.add(w.user_id);
|
||||
W.workerNames[w.user_id] = w.worker_name;
|
||||
});
|
||||
}
|
||||
renderStepWorkers(document.getElementById('stepContainer'));
|
||||
};
|
||||
|
||||
// --- Step 2: 프로젝트 + 공정 선택 (통합) ---
|
||||
function renderStepProjectAndWorkType(container) {
|
||||
var projects = window.TbmState.allProjects;
|
||||
var workTypes = window.TbmState.allWorkTypes;
|
||||
|
||||
// 프로젝트 선택 UI
|
||||
var skipSelected = W.projectId === null ? ' selected' : '';
|
||||
var projectItems = projects.map(function(p) {
|
||||
var selected = W.projectId === p.project_id ? ' selected' : '';
|
||||
return '<div class="list-item' + selected + '" data-action="selectProject" data-project-id="' + p.project_id + '" data-project-name="' + esc(p.project_name) + '">' +
|
||||
'<div class="item-title">' + esc(p.project_name) + '</div>' +
|
||||
'<div class="item-desc">' + esc(p.job_no || '') + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
// 공정 pill 버튼
|
||||
var pillHtml = workTypes.map(function(wt) {
|
||||
var selected = W.workTypeId === wt.id ? ' selected' : '';
|
||||
return '<button type="button" class="pill-btn' + selected + '" data-action="selectWorkType" data-wt-id="' + wt.id + '" data-wt-name="' + esc(wt.name) + '">' + esc(wt.name) + '</button>';
|
||||
}).join('');
|
||||
pillHtml += '<button type="button" class="pill-btn-add" onclick="toggleAddWorkType()">+ 추가</button>';
|
||||
|
||||
// 공정 인라인 추가 폼
|
||||
var addWorkTypeFormHtml = '';
|
||||
if (W.showAddWorkType) {
|
||||
addWorkTypeFormHtml =
|
||||
'<div class="inline-add-form" id="addWorkTypeForm">' +
|
||||
'<input type="text" id="newWorkTypeName" placeholder="새 공정명 입력" autocomplete="off">' +
|
||||
'<div class="inline-add-btns">' +
|
||||
'<button type="button" class="btn-cancel" onclick="cancelAddWorkType()">취소</button>' +
|
||||
'<button type="button" class="btn-save" id="btnSaveWorkType" onclick="saveNewWorkType()">저장</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="wizard-section">' +
|
||||
'<div class="section-title"><span class="sn">2</span>프로젝트 선택 <span style="font-size:0.75rem;font-weight:400;color:#9ca3af;">(선택사항)</span></div>' +
|
||||
'<div class="list-item-skip' + skipSelected + '" data-action="selectProject" data-project-id="" data-project-name="">' +
|
||||
'선택 안함' +
|
||||
'</div>' +
|
||||
(projects.length > 0 ? projectItems : '<div class="empty-state">등록된 프로젝트가 없습니다</div>') +
|
||||
'</div>' +
|
||||
'<div class="wizard-section">' +
|
||||
'<div class="section-title"><span class="sn">2</span>공정 선택 <span style="font-size:0.75rem;font-weight:400;color:#ef4444;">(필수)</span></div>' +
|
||||
'<div class="pill-grid">' + pillHtml + '</div>' +
|
||||
addWorkTypeFormHtml +
|
||||
'</div>';
|
||||
|
||||
// 자동 포커스
|
||||
if (W.showAddWorkType) {
|
||||
var inp = document.getElementById('newWorkTypeName');
|
||||
if (inp) {
|
||||
setTimeout(function() { inp.focus(); }, 50);
|
||||
inp.onkeydown = function(e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); saveNewWorkType(); }
|
||||
if (e.key === 'Escape') { cancelAddWorkType(); }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Event delegation for project/workType selection
|
||||
container.onclick = function(e) {
|
||||
var el = e.target.closest('[data-action]');
|
||||
if (!el) return;
|
||||
var action = el.getAttribute('data-action');
|
||||
if (action === 'selectProject') {
|
||||
var pid = el.getAttribute('data-project-id');
|
||||
selectProject(pid ? parseInt(pid) : null, el.getAttribute('data-project-name') || '');
|
||||
} else if (action === 'selectWorkType') {
|
||||
selectWorkType(parseInt(el.getAttribute('data-wt-id')), el.getAttribute('data-wt-name') || '');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.selectProject = function(projectId, projectName) {
|
||||
W.projectId = projectId;
|
||||
W.projectName = projectName || '';
|
||||
// Update project list items
|
||||
document.querySelectorAll('#stepContainer .list-item, #stepContainer .list-item-skip').forEach(function(el) {
|
||||
el.classList.remove('selected');
|
||||
});
|
||||
if (projectId === null) {
|
||||
var skipEl = document.querySelector('#stepContainer .list-item-skip');
|
||||
if (skipEl) skipEl.classList.add('selected');
|
||||
} else {
|
||||
document.querySelectorAll('#stepContainer .list-item').forEach(function(el) {
|
||||
var title = el.querySelector('.item-title');
|
||||
if (title && title.textContent === projectName) {
|
||||
el.classList.add('selected');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.selectWorkType = function(id, name) {
|
||||
console.log('[TBM Create] selectWorkType:', id, name);
|
||||
W.workTypeId = id;
|
||||
W.workTypeName = name;
|
||||
// Update pill buttons
|
||||
document.querySelectorAll('#stepContainer .pill-btn').forEach(function(el) {
|
||||
el.classList.remove('selected');
|
||||
});
|
||||
document.querySelectorAll('#stepContainer .pill-btn').forEach(function(el) {
|
||||
if (el.textContent === name) {
|
||||
el.classList.add('selected');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// --- Step 2: 인라인 추가 (공정) ---
|
||||
|
||||
window.toggleAddWorkType = function() {
|
||||
W.showAddWorkType = !W.showAddWorkType;
|
||||
renderStepProjectAndWorkType(document.getElementById('stepContainer'));
|
||||
};
|
||||
|
||||
window.cancelAddWorkType = function() {
|
||||
W.showAddWorkType = false;
|
||||
renderStepProjectAndWorkType(document.getElementById('stepContainer'));
|
||||
};
|
||||
|
||||
window.saveNewWorkType = async function() {
|
||||
var inp = document.getElementById('newWorkTypeName');
|
||||
var btn = document.getElementById('btnSaveWorkType');
|
||||
if (!inp || !btn) return;
|
||||
|
||||
var name = inp.value.trim();
|
||||
if (!name) {
|
||||
showToast('공정명을 입력해주세요.', 'warning');
|
||||
inp.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
var exists = window.TbmState.allWorkTypes.some(function(wt) {
|
||||
return wt.name.toLowerCase() === name.toLowerCase();
|
||||
});
|
||||
if (exists) {
|
||||
showToast('이미 존재하는 공정명입니다.', 'warning');
|
||||
inp.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = '저장 중...';
|
||||
|
||||
try {
|
||||
var response = await window.apiCall('/daily-work-reports/work-types', 'POST', { name: name });
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '공정 추가 실패');
|
||||
}
|
||||
|
||||
var newItem = response.data;
|
||||
window.TbmState.allWorkTypes.push(newItem);
|
||||
|
||||
W.workTypeId = newItem.id;
|
||||
W.workTypeName = newItem.name;
|
||||
W.showAddWorkType = false;
|
||||
|
||||
renderStepProjectAndWorkType(document.getElementById('stepContainer'));
|
||||
showToast('\'' + name + '\' 공정이 추가되었습니다.', 'success');
|
||||
} catch (error) {
|
||||
console.error('공정 추가 오류:', error);
|
||||
showToast('공정 추가 중 오류: ' + error.message, 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = '저장';
|
||||
}
|
||||
};
|
||||
|
||||
// --- Step 3: 확인 ---
|
||||
function renderStepConfirm(container) {
|
||||
var dateDisplay = window.TbmUtils.formatDateFull(W.sessionDate);
|
||||
|
||||
// 작업자 이름 목록
|
||||
var workerNameList = [];
|
||||
W.workers.forEach(function(wid) {
|
||||
workerNameList.push(W.workerNames[wid] || '작업자');
|
||||
});
|
||||
|
||||
var summaryHtml =
|
||||
'<div class="summary-card">' +
|
||||
'<div class="summary-row"><span class="summary-label">날짜</span><span class="summary-value">' + esc(dateDisplay) + '</span></div>' +
|
||||
'<div class="summary-row"><span class="summary-label">입력자</span><span class="summary-value">' + esc(W.leaderName || '(미설정)') + '</span></div>' +
|
||||
'<div class="summary-row"><span class="summary-label">프로젝트</span><span class="summary-value">' + esc(W.projectName || '선택 안함') + '</span></div>' +
|
||||
'<div class="summary-row"><span class="summary-label">공정</span><span class="summary-value">' + esc(W.workTypeName) + '</span></div>' +
|
||||
'<div class="summary-row"><span class="summary-label">작업자</span><span class="summary-value">' + W.workers.size + '명</span></div>' +
|
||||
'</div>';
|
||||
|
||||
// 작업자 목록 (간단 표시)
|
||||
var workerListHtml = workerNameList.map(function(name) {
|
||||
return '<div style="display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;background:#f9fafb;border-radius:0.5rem;margin-bottom:0.25rem;">' +
|
||||
'<span style="font-size:0.875rem;font-weight:500;color:#1f2937;">' + esc(name) + '</span>' +
|
||||
'<span style="font-size:0.6875rem;color:#9ca3af;margin-left:auto;">세부 미입력</span>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="wizard-section">' +
|
||||
'<div class="section-title"><span class="sn">3</span>확인</div>' +
|
||||
summaryHtml +
|
||||
'</div>' +
|
||||
'<div class="wizard-section">' +
|
||||
'<div class="section-title">작업자 목록</div>' +
|
||||
'<div style="padding:0.5rem;background:#fff7ed;border:1px solid #fed7aa;border-radius:0.5rem;margin-bottom:0.75rem;font-size:0.8125rem;color:#c2410c;">' +
|
||||
'저장 후 TBM 카드를 탭하면 작업자별 작업/작업장을 입력할 수 있습니다.' +
|
||||
'</div>' +
|
||||
workerListHtml +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// ==================== 저장 ====================
|
||||
|
||||
var _saving = false;
|
||||
async function saveWizard() {
|
||||
if (_saving) return;
|
||||
_saving = true;
|
||||
// 로딩 오버레이 표시
|
||||
var overlay = document.getElementById('loadingOverlay');
|
||||
var loadingText = document.getElementById('loadingText');
|
||||
if (overlay) {
|
||||
if (loadingText) loadingText.textContent = '저장 중...';
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
// 저장 버튼 비활성화
|
||||
var saveBtn = document.getElementById('nextBtn');
|
||||
if (saveBtn) {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = '저장 중...';
|
||||
}
|
||||
|
||||
try {
|
||||
var leaderId = W.leaderId ? parseInt(W.leaderId) : null;
|
||||
|
||||
// 1. TBM 세션 생성
|
||||
var sessionData = {
|
||||
session_date: W.sessionDate,
|
||||
leader_user_id: leaderId
|
||||
};
|
||||
|
||||
var response = await window.apiCall('/tbm/sessions', 'POST', sessionData);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '세션 생성 실패');
|
||||
}
|
||||
|
||||
var sessionId = response.data.session_id;
|
||||
|
||||
// 2. 팀원 일괄 추가 (task_id, workplace_id = null)
|
||||
var members = [];
|
||||
W.workers.forEach(function(wid) {
|
||||
members.push({
|
||||
user_id: wid,
|
||||
project_id: W.projectId,
|
||||
work_type_id: W.workTypeId,
|
||||
task_id: null,
|
||||
workplace_category_id: null,
|
||||
workplace_id: null,
|
||||
work_detail: null,
|
||||
is_present: true
|
||||
});
|
||||
});
|
||||
|
||||
var teamResponse = await window.apiCall(
|
||||
'/tbm/sessions/' + sessionId + '/team/batch',
|
||||
'POST',
|
||||
{ members: members }
|
||||
);
|
||||
|
||||
if (!teamResponse || !teamResponse.success) {
|
||||
var err = new Error(teamResponse?.message || '팀원 추가 실패');
|
||||
if (teamResponse && teamResponse.duplicates) err.duplicates = teamResponse.duplicates;
|
||||
err._sessionId = sessionId;
|
||||
throw err;
|
||||
}
|
||||
|
||||
showToast('TBM이 생성되었습니다 (작업자 ' + members.length + '명)', 'success');
|
||||
|
||||
// 3. tbm-mobile.html로 이동
|
||||
setTimeout(function() {
|
||||
window.location.href = '/pages/work/tbm-mobile.html';
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('TBM 저장 오류:', error);
|
||||
|
||||
// 409 중복 배정 에러 처리
|
||||
if (error.duplicates && error.duplicates.length > 0) {
|
||||
// 고아 세션 삭제
|
||||
if (error._sessionId) {
|
||||
try { await window.apiCall('/tbm/sessions/' + error._sessionId, 'DELETE'); } catch(e) {}
|
||||
}
|
||||
// 중복 작업자 자동 해제
|
||||
error.duplicates.forEach(function(d) {
|
||||
W.workers.delete(d.user_id);
|
||||
delete W.workerNames[d.user_id];
|
||||
});
|
||||
// 배정 현황 캐시 갱신
|
||||
W.todayAssignments = null;
|
||||
// Step 1로 복귀
|
||||
W.step = 1;
|
||||
renderStep(1);
|
||||
updateIndicator();
|
||||
updateNav();
|
||||
showToast(error.message, 'error');
|
||||
} else {
|
||||
showToast('TBM 저장 중 오류가 발생했습니다: ' + error.message, 'error');
|
||||
}
|
||||
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
if (saveBtn) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '저장';
|
||||
}
|
||||
_saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 토스트 (로컬) ====================
|
||||
|
||||
function showToast(message, type) {
|
||||
if (window.showToast && typeof window.showToast === 'function') {
|
||||
window.showToast(message, type);
|
||||
return;
|
||||
}
|
||||
console.log('[Toast] ' + type + ': ' + message);
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user