sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거, department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러, 4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
589 lines
21 KiB
JavaScript
589 lines
21 KiB
JavaScript
/**
|
|
* 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) {
|
|
if (user.user_id) {
|
|
var worker = window.TbmState.allWorkers.find(function(w) { return w.user_id === user.user_id; });
|
|
if (worker) {
|
|
W.leaderId = worker.user_id;
|
|
W.leaderName = worker.worker_name;
|
|
} else {
|
|
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.total_hours >= 8;
|
|
var partiallyAssigned = assignment && assignment.total_hours > 0 && assignment.total_hours < 8;
|
|
|
|
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 (' + assignment.total_hours + 'h)</div>';
|
|
disabledClass = ' disabled';
|
|
onclick = '';
|
|
} else if (partiallyAssigned) {
|
|
var remaining = 8 - assignment.total_hours;
|
|
badgeHtml = '<div style="font-size:0.625rem; color:#2563eb; margin-top:0.125rem;">' + remaining + 'h 가용</div>';
|
|
}
|
|
|
|
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.total_hours >= 8) 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.total_hours >= 8);
|
|
});
|
|
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 + '" onclick="selectProject(' + p.project_id + ', \'' + esc(p.project_name).replace(/'/g, "\\'") + '\')">' +
|
|
'<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 + '" onclick="selectWorkType(' + wt.id + ', \'' + esc(wt.name).replace(/'/g, "\\'") + '\')">' + 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 + '" onclick="selectProject(null, \'\')">' +
|
|
'선택 안함' +
|
|
'</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(); }
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
throw new Error(teamResponse?.message || '팀원 추가 실패');
|
|
}
|
|
|
|
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);
|
|
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);
|
|
}
|
|
|
|
})();
|