feat: 모바일 UX 대폭 개선 + PWA 구현 + 로그인 루프 수정
- 모바일 하단 네비: 메뉴 제거, 4개 핵심 기능(홈/TBM/작업보고/출근) SVG 아이콘 - 모바일 사이드바 스킵: 768px 이하에서 사이드바 미로드, 레이아웃 오프셋 해결 - 모바일 헤더: 햄버거 메뉴 숨김, 본문 margin/overflow 정리 - TBM 모바일: 풀스크린 모달, 저장 버튼 하단 고정, 터치 UX 개선 - PWA: manifest.json, sw.js(network-first), 앱 아이콘, iOS 메타태그, 킬스위치 - 로그인 무한루프 수정: 토큰 만료 검증, 쿠키 정리, loginPage 경로 수정 - 신고 메뉴 tkreport 리다이렉트: navbar + sidebar cross-system-link 적용 - TBM API: 작업장별 안전점검 체크리스트 조회 엔드포인트 추가 - 안전점검 체크리스트 관리 UI 개선 - tkuser: 이슈유형 관리 기능 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -128,7 +128,8 @@
|
||||
var url = window.API_BASE_URL + endpoint;
|
||||
var config = {
|
||||
method: method,
|
||||
headers: window.getAuthHeaders()
|
||||
headers: window.getAuthHeaders(),
|
||||
cache: 'no-store'
|
||||
};
|
||||
|
||||
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE')) {
|
||||
|
||||
@@ -77,6 +77,12 @@ function clearAuthData() {
|
||||
localStorage.removeItem('sso_user');
|
||||
localStorage.removeItem('userInfo');
|
||||
localStorage.removeItem('currentUser');
|
||||
// SSO 쿠키도 삭제 (로그인 페이지 자동 리다이렉트 방지)
|
||||
var cookieDomain = window.location.hostname.includes('technicalkorea.net')
|
||||
? '; domain=.technicalkorea.net' : '';
|
||||
document.cookie = 'sso_token=; path=/; max-age=0' + cookieDomain;
|
||||
document.cookie = 'sso_user=; path=/; max-age=0' + cookieDomain;
|
||||
document.cookie = 'sso_refresh_token=; path=/; max-age=0' + cookieDomain;
|
||||
}
|
||||
|
||||
function getAuthHeaders() {
|
||||
|
||||
@@ -491,28 +491,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 사이드바 컨테이너 생성 (없으면)
|
||||
let sidebarContainer = document.getElementById('sidebar-container');
|
||||
if (!sidebarContainer) {
|
||||
sidebarContainer = document.createElement('div');
|
||||
sidebarContainer.id = 'sidebar-container';
|
||||
document.body.prepend(sidebarContainer);
|
||||
console.log('📦 사이드바 컨테이너 생성됨');
|
||||
// 3. 네비바 로드 (모바일이면 사이드바 스킵)
|
||||
var isMobile = window.innerWidth <= 768;
|
||||
|
||||
if (!isMobile) {
|
||||
// 데스크톱: 사이드바 컨테이너 생성 및 로드
|
||||
let sidebarContainer = document.getElementById('sidebar-container');
|
||||
if (!sidebarContainer) {
|
||||
sidebarContainer = document.createElement('div');
|
||||
sidebarContainer.id = 'sidebar-container';
|
||||
document.body.prepend(sidebarContainer);
|
||||
}
|
||||
|
||||
console.log('📥 컴포넌트 로딩 시작 (데스크톱: 네비바+사이드바)');
|
||||
await Promise.all([
|
||||
loadComponent('navbar', '#navbar-container', (doc) => processNavbar(doc, currentUser, accessiblePageKeys)),
|
||||
loadComponent('sidebar-nav', '#sidebar-container', (doc) => processSidebar(doc, currentUser, accessiblePageKeys))
|
||||
]);
|
||||
|
||||
setupNavbarEvents();
|
||||
setupSidebarEvents();
|
||||
document.body.classList.add('has-sidebar');
|
||||
} else {
|
||||
// 모바일: 네비바만 로드, 사이드바 없음
|
||||
console.log('📥 컴포넌트 로딩 시작 (모바일: 네비바만)');
|
||||
await loadComponent('navbar', '#navbar-container', (doc) => processNavbar(doc, currentUser, accessiblePageKeys));
|
||||
setupNavbarEvents();
|
||||
}
|
||||
|
||||
// 4. 네비바와 사이드바 동시 로드
|
||||
console.log('📥 컴포넌트 로딩 시작');
|
||||
await Promise.all([
|
||||
loadComponent('navbar', '#navbar-container', (doc) => processNavbar(doc, currentUser, accessiblePageKeys)),
|
||||
loadComponent('sidebar-nav', '#sidebar-container', (doc) => processSidebar(doc, currentUser, accessiblePageKeys))
|
||||
]);
|
||||
console.log('✅ 컴포넌트 로딩 완료');
|
||||
|
||||
// 5. 이벤트 설정
|
||||
setupNavbarEvents();
|
||||
setupSidebarEvents();
|
||||
document.body.classList.add('has-sidebar');
|
||||
|
||||
// 6. 페이지 전환 로딩 인디케이터 설정
|
||||
setupPageTransitionLoader();
|
||||
|
||||
@@ -527,9 +534,69 @@
|
||||
setTimeout(loadNotifications, 200);
|
||||
setInterval(loadNotifications, 30000);
|
||||
|
||||
// 10. PWA 설정 (manifest + 서비스 워커 + iOS 메타태그)
|
||||
setupPWA();
|
||||
|
||||
console.log('✅ app-init 완료');
|
||||
}
|
||||
|
||||
// ===== PWA 설정 =====
|
||||
function setupPWA() {
|
||||
// manifest.json 동적 추가
|
||||
if (!document.querySelector('link[rel="manifest"]')) {
|
||||
var manifest = document.createElement('link');
|
||||
manifest.rel = 'manifest';
|
||||
manifest.href = '/manifest.json';
|
||||
document.head.appendChild(manifest);
|
||||
}
|
||||
|
||||
// iOS 홈 화면 앱 메타태그
|
||||
if (!document.querySelector('meta[name="apple-mobile-web-app-capable"]')) {
|
||||
var metaTags = [
|
||||
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
|
||||
{ name: 'apple-mobile-web-app-status-bar-style', content: 'default' },
|
||||
{ name: 'apple-mobile-web-app-title', content: 'TK공장' },
|
||||
{ name: 'theme-color', content: '#1e40af' }
|
||||
];
|
||||
metaTags.forEach(function(tag) {
|
||||
var meta = document.createElement('meta');
|
||||
meta.name = tag.name;
|
||||
meta.content = tag.content;
|
||||
document.head.appendChild(meta);
|
||||
});
|
||||
|
||||
// iOS 아이콘
|
||||
var appleIcon = document.createElement('link');
|
||||
appleIcon.rel = 'apple-touch-icon';
|
||||
appleIcon.href = '/img/icon-192x192.png';
|
||||
document.head.appendChild(appleIcon);
|
||||
}
|
||||
|
||||
// 서비스 워커 등록 (킬스위치 포함)
|
||||
if ('serviceWorker' in navigator) {
|
||||
// 킬스위치: ?sw-kill 파라미터로 서비스 워커 해제
|
||||
if (window.location.search.includes('sw-kill')) {
|
||||
navigator.serviceWorker.getRegistrations().then(function(regs) {
|
||||
regs.forEach(function(r) { r.unregister(); });
|
||||
caches.keys().then(function(keys) {
|
||||
keys.forEach(function(k) { caches.delete(k); });
|
||||
});
|
||||
console.log('SW 해제 완료');
|
||||
window.location.replace(window.location.pathname);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(function(reg) {
|
||||
console.log('SW 등록 완료');
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.warn('SW 등록 실패:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 페이지 전환 로딩 인디케이터 =====
|
||||
function setupPageTransitionLoader() {
|
||||
// 로딩 바 스타일 추가
|
||||
|
||||
@@ -15,7 +15,7 @@ export const config = {
|
||||
// 페이지 경로 설정
|
||||
paths: {
|
||||
// 로그인 페이지 경로
|
||||
loginPage: '/index.html',
|
||||
loginPage: '/login',
|
||||
// 메인 대시보드 경로 (모든 사용자 공통)
|
||||
dashboard: '/pages/dashboard.html',
|
||||
// 하위 호환성을 위한 별칭들
|
||||
|
||||
@@ -146,7 +146,7 @@ function populateWorkTypeSelects() {
|
||||
const modalSelect = document.getElementById('modalWorkType');
|
||||
|
||||
const options = workTypes.map(wt =>
|
||||
`<option value="${wt.work_type_id}">${wt.work_type_name}</option>`
|
||||
`<option value="${wt.id}">${wt.name}</option>`
|
||||
).join('');
|
||||
|
||||
if (filterSelect) {
|
||||
@@ -204,7 +204,7 @@ function renderBasicChecks() {
|
||||
console.log('기본 체크항목:', basicChecks.length, '개');
|
||||
|
||||
if (basicChecks.length === 0) {
|
||||
container.innerHTML = renderEmptyState('기본 체크 항목이 없습니다.');
|
||||
container.innerHTML = renderEmptyState('기본 체크 항목이 없습니다.') + renderInlineAddStandalone('basic');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ function renderBasicChecks() {
|
||||
|
||||
container.innerHTML = Object.entries(grouped).map(([category, items]) =>
|
||||
renderChecklistGroup(category, items)
|
||||
).join('');
|
||||
).join('') + renderInlineAddStandalone('basic');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,8 +229,10 @@ function renderWeatherChecks() {
|
||||
weatherChecks = weatherChecks.filter(c => c.weather_condition === filterValue);
|
||||
}
|
||||
|
||||
const inlineRow = filterValue ? renderInlineAddStandalone('weather') : '';
|
||||
|
||||
if (weatherChecks.length === 0) {
|
||||
container.innerHTML = renderEmptyState('날씨별 체크 항목이 없습니다.');
|
||||
container.innerHTML = renderEmptyState('날씨별 체크 항목이 없습니다.') + inlineRow;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -243,7 +245,7 @@ function renderWeatherChecks() {
|
||||
const name = conditionInfo?.condition_name || condition;
|
||||
|
||||
return renderChecklistGroup(`${icon} ${name}`, items, condition);
|
||||
}).join('');
|
||||
}).join('') + inlineRow;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,6 +256,12 @@ function renderTaskChecks() {
|
||||
const workTypeId = document.getElementById('workTypeFilter')?.value;
|
||||
const taskId = document.getElementById('taskFilter')?.value;
|
||||
|
||||
// 공정 미선택 시 안내
|
||||
if (!workTypeId) {
|
||||
container.innerHTML = renderGuideState('공정을 먼저 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
let taskChecks = allChecks.filter(c => c.check_type === 'task');
|
||||
|
||||
if (taskId) {
|
||||
@@ -264,8 +272,10 @@ function renderTaskChecks() {
|
||||
taskChecks = taskChecks.filter(c => taskIds.includes(c.task_id));
|
||||
}
|
||||
|
||||
const inlineRow = taskId ? renderInlineAddStandalone('task') : '';
|
||||
|
||||
if (taskChecks.length === 0) {
|
||||
container.innerHTML = renderEmptyState('작업별 체크 항목이 없습니다.');
|
||||
container.innerHTML = renderEmptyState('작업별 체크 항목이 없습니다.') + inlineRow;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,7 +287,7 @@ function renderTaskChecks() {
|
||||
const taskName = task?.task_name || `작업 ${taskId}`;
|
||||
|
||||
return renderChecklistGroup(`📋 ${taskName}`, items, null, taskId);
|
||||
}).join('');
|
||||
}).join('') + inlineRow;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,6 +394,18 @@ function renderEmptyState(message) {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 안내 상태 렌더링 (필터 미선택 시)
|
||||
*/
|
||||
function renderGuideState(message) {
|
||||
return `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">👆</div>
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨 필터 변경
|
||||
*/
|
||||
@@ -409,7 +431,7 @@ async function filterByWorkType() {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall(`/tasks/work-type/${workTypeId}`);
|
||||
const response = await apiCall(`/tasks/by-work-type/${workTypeId}`);
|
||||
if (response && response.success) {
|
||||
tasks = response.data || [];
|
||||
taskSelect.innerHTML = '<option value="">작업 선택</option>' +
|
||||
@@ -446,7 +468,7 @@ async function loadModalTasks() {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall(`/tasks/work-type/${workTypeId}`);
|
||||
const response = await apiCall(`/tasks/by-work-type/${workTypeId}`);
|
||||
if (response && response.success) {
|
||||
const modalTasks = response.data || [];
|
||||
taskSelect.innerHTML = '<option value="">작업 선택</option>' +
|
||||
@@ -486,6 +508,29 @@ function openAddModal() {
|
||||
}
|
||||
|
||||
toggleConditionalFields();
|
||||
|
||||
// 날씨별 탭: 현재 필터의 날씨 조건 반영
|
||||
if (currentTab === 'weather') {
|
||||
const weatherFilter = document.getElementById('weatherFilter')?.value;
|
||||
if (weatherFilter) {
|
||||
document.getElementById('weatherCondition').value = weatherFilter;
|
||||
}
|
||||
}
|
||||
|
||||
// 작업별 탭: 현재 필터의 공정/작업 반영
|
||||
if (currentTab === 'task') {
|
||||
const workTypeId = document.getElementById('workTypeFilter')?.value;
|
||||
if (workTypeId) {
|
||||
document.getElementById('modalWorkType').value = workTypeId;
|
||||
loadModalTasks().then(() => {
|
||||
const taskId = document.getElementById('taskFilter')?.value;
|
||||
if (taskId) {
|
||||
document.getElementById('modalTask').value = taskId;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showModal();
|
||||
}
|
||||
|
||||
@@ -660,6 +705,132 @@ async function deleteCheck(checkId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 인라인 추가 행 렌더링
|
||||
*/
|
||||
function renderInlineAddRow(tabType) {
|
||||
if (tabType === 'basic') {
|
||||
const categoryOptions = Object.entries(CATEGORIES)
|
||||
.filter(([key]) => !['WEATHER', 'TASK'].includes(key))
|
||||
.map(([key, val]) => `<option value="${key}">${val.name}</option>`)
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<div class="inline-add-row">
|
||||
<select class="inline-add-select" id="inlineCategory">${categoryOptions}</select>
|
||||
<input type="text" class="inline-add-input" id="inlineBasicInput"
|
||||
placeholder="새 체크 항목 입력..." onkeydown="if(event.key==='Enter'){event.preventDefault();addInlineCheck('basic');}">
|
||||
<button class="inline-add-btn" onclick="addInlineCheck('basic')">추가</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (tabType === 'weather') {
|
||||
return `
|
||||
<div class="inline-add-row">
|
||||
<input type="text" class="inline-add-input" id="inlineWeatherInput"
|
||||
placeholder="새 날씨별 체크 항목 입력..." onkeydown="if(event.key==='Enter'){event.preventDefault();addInlineCheck('weather');}">
|
||||
<button class="inline-add-btn" onclick="addInlineCheck('weather')">추가</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (tabType === 'task') {
|
||||
return `
|
||||
<div class="inline-add-row">
|
||||
<input type="text" class="inline-add-input" id="inlineTaskInput"
|
||||
placeholder="새 작업별 체크 항목 입력..." onkeydown="if(event.key==='Enter'){event.preventDefault();addInlineCheck('task');}">
|
||||
<button class="inline-add-btn" onclick="addInlineCheck('task')">추가</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 인라인 추가 행을 standalone 컨테이너로 감싸기 (빈 상태용)
|
||||
*/
|
||||
function renderInlineAddStandalone(tabType) {
|
||||
return `<div class="inline-add-standalone">${renderInlineAddRow(tabType)}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인라인으로 체크 항목 추가
|
||||
*/
|
||||
async function addInlineCheck(tabType) {
|
||||
let checkItem, data;
|
||||
|
||||
if (tabType === 'basic') {
|
||||
const input = document.getElementById('inlineBasicInput');
|
||||
const categorySelect = document.getElementById('inlineCategory');
|
||||
checkItem = input?.value.trim();
|
||||
if (!checkItem) { input?.focus(); return; }
|
||||
|
||||
data = {
|
||||
check_type: 'basic',
|
||||
check_item: checkItem,
|
||||
check_category: categorySelect?.value || 'PPE',
|
||||
is_required: true,
|
||||
display_order: 0
|
||||
};
|
||||
} else if (tabType === 'weather') {
|
||||
const input = document.getElementById('inlineWeatherInput');
|
||||
checkItem = input?.value.trim();
|
||||
if (!checkItem) { input?.focus(); return; }
|
||||
|
||||
const weatherFilter = document.getElementById('weatherFilter')?.value;
|
||||
if (!weatherFilter) {
|
||||
showToast('날씨 조건을 먼저 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
data = {
|
||||
check_type: 'weather',
|
||||
check_item: checkItem,
|
||||
check_category: 'WEATHER',
|
||||
weather_condition: weatherFilter,
|
||||
is_required: true,
|
||||
display_order: 0
|
||||
};
|
||||
} else if (tabType === 'task') {
|
||||
const input = document.getElementById('inlineTaskInput');
|
||||
checkItem = input?.value.trim();
|
||||
if (!checkItem) { input?.focus(); return; }
|
||||
|
||||
const taskId = document.getElementById('taskFilter')?.value;
|
||||
if (!taskId) {
|
||||
showToast('작업을 먼저 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
data = {
|
||||
check_type: 'task',
|
||||
check_item: checkItem,
|
||||
check_category: 'TASK',
|
||||
task_id: parseInt(taskId),
|
||||
is_required: true,
|
||||
display_order: 0
|
||||
};
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall('/tbm/safety-checks', 'POST', data);
|
||||
if (response && response.success) {
|
||||
showToast('항목이 추가되었습니다.', 'success');
|
||||
await loadAllChecks();
|
||||
renderCurrentTab();
|
||||
} else {
|
||||
showToast(response?.message || '추가에 실패했습니다.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('인라인 추가 실패:', error);
|
||||
showToast('추가 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토스트 메시지 표시
|
||||
*/
|
||||
@@ -716,3 +887,4 @@ window.filterByWorkType = filterByWorkType;
|
||||
window.filterByTask = filterByTask;
|
||||
window.loadModalTasks = loadModalTasks;
|
||||
window.toggleConditionalFields = toggleConditionalFields;
|
||||
window.addInlineCheck = addInlineCheck;
|
||||
|
||||
@@ -31,6 +31,31 @@ let loadedDaysCount = 7; // 처음에 로드할 일수
|
||||
let dateGroupedSessions = {}; // 날짜별로 그룹화된 세션
|
||||
let allLoadedSessions = []; // 전체 로드된 세션
|
||||
|
||||
// 모달 스크롤 잠금
|
||||
let scrollLockY = 0;
|
||||
let scrollLockCount = 0;
|
||||
function lockBodyScroll() {
|
||||
scrollLockCount++;
|
||||
if (scrollLockCount > 1) return; // 이미 잠금 상태
|
||||
scrollLockY = window.scrollY;
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.body.style.position = 'fixed';
|
||||
document.body.style.width = '100%';
|
||||
document.body.style.top = `-${scrollLockY}px`;
|
||||
document.body.classList.add('tbm-modal-open');
|
||||
}
|
||||
function unlockBodyScroll() {
|
||||
scrollLockCount--;
|
||||
if (scrollLockCount > 0) return; // 아직 열린 모달 있음
|
||||
scrollLockCount = 0;
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.position = '';
|
||||
document.body.style.width = '';
|
||||
document.body.style.top = '';
|
||||
window.scrollTo(0, scrollLockY);
|
||||
document.body.classList.remove('tbm-modal-open');
|
||||
}
|
||||
|
||||
// ==================== 유틸리티 함수 ====================
|
||||
|
||||
/**
|
||||
@@ -541,11 +566,14 @@ function createSessionCard(session) {
|
||||
${session.status === 'draft' ? `
|
||||
<div class="tbm-card-footer">
|
||||
<button class="tbm-btn tbm-btn-primary tbm-btn-sm" onclick="event.stopPropagation(); openTeamCompositionModal(${safeSessionId})">
|
||||
👥 팀 구성
|
||||
👥 수정
|
||||
</button>
|
||||
<button class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="event.stopPropagation(); openSafetyCheckModal(${safeSessionId})">
|
||||
✓ 안전 체크
|
||||
</button>
|
||||
<button class="tbm-btn tbm-btn-danger tbm-btn-sm" onclick="event.stopPropagation(); confirmDeleteTbm(${safeSessionId})">
|
||||
🗑 삭제
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -591,7 +619,7 @@ function openNewTbmModal() {
|
||||
renderWorkerTaskList();
|
||||
|
||||
document.getElementById('tbmModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
lockBodyScroll();
|
||||
}
|
||||
window.openNewTbmModal = openNewTbmModal;
|
||||
|
||||
@@ -697,7 +725,7 @@ window.loadTasksByWorkType = loadTasksByWorkType;
|
||||
// TBM 모달 닫기
|
||||
function closeTbmModal() {
|
||||
document.getElementById('tbmModal').style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
unlockBodyScroll();
|
||||
}
|
||||
window.closeTbmModal = closeTbmModal;
|
||||
|
||||
@@ -915,7 +943,7 @@ function renderTaskLine(workerData, workerIndex, taskLine, taskIndex) {
|
||||
|
||||
return `
|
||||
<div style="padding: 0.75rem; margin-bottom: 0.5rem; background: #f9fafb; border-radius: 0.5rem; border: 1px solid #e5e7eb;">
|
||||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<div class="tbm-task-grid" style="margin-bottom: 0.5rem;">
|
||||
<!-- 프로젝트 선택 -->
|
||||
<button type="button"
|
||||
onclick="openItemSelect('project', ${safeWorkerIndex}, ${safeTaskIndex})"
|
||||
@@ -992,6 +1020,7 @@ function openWorkerSelectionModal() {
|
||||
}).join('');
|
||||
|
||||
document.getElementById('workerSelectionModal').style.display = 'flex';
|
||||
lockBodyScroll();
|
||||
}
|
||||
window.openWorkerSelectionModal = openWorkerSelectionModal;
|
||||
|
||||
@@ -1090,6 +1119,7 @@ window.confirmWorkerSelection = confirmWorkerSelection;
|
||||
// 작업자 선택 모달 닫기
|
||||
function closeWorkerSelectionModal() {
|
||||
document.getElementById('workerSelectionModal').style.display = 'none';
|
||||
unlockBodyScroll();
|
||||
selectedWorkersInModal.clear();
|
||||
}
|
||||
window.closeWorkerSelectionModal = closeWorkerSelectionModal;
|
||||
@@ -1168,6 +1198,7 @@ function openBulkSettingModal() {
|
||||
document.getElementById('bulkWorkplaceBtn').classList.add('btn-secondary');
|
||||
|
||||
document.getElementById('bulkSettingModal').style.display = 'flex';
|
||||
lockBodyScroll();
|
||||
}
|
||||
window.openBulkSettingModal = openBulkSettingModal;
|
||||
|
||||
@@ -1221,6 +1252,7 @@ window.deselectAllForBulk = deselectAllForBulk;
|
||||
// 일괄 설정 모달 닫기
|
||||
function closeBulkSettingModal() {
|
||||
document.getElementById('bulkSettingModal').style.display = 'none';
|
||||
unlockBodyScroll();
|
||||
isBulkMode = false;
|
||||
}
|
||||
window.closeBulkSettingModal = closeBulkSettingModal;
|
||||
@@ -1279,6 +1311,7 @@ function openBulkItemSelect(type) {
|
||||
`).join('') : '<div style="text-align: center; padding: 2rem; color: #9ca3af;">선택 가능한 항목이 없습니다</div>';
|
||||
|
||||
modal.style.display = 'flex';
|
||||
lockBodyScroll();
|
||||
}
|
||||
window.openBulkItemSelect = openBulkItemSelect;
|
||||
|
||||
@@ -1318,6 +1351,7 @@ function openBulkWorkplaceSelect() {
|
||||
isBulkMode = true;
|
||||
loadWorkplaceCategories();
|
||||
document.getElementById('workplaceSelectModal').style.display = 'flex';
|
||||
lockBodyScroll();
|
||||
}
|
||||
window.openBulkWorkplaceSelect = openBulkWorkplaceSelect;
|
||||
|
||||
@@ -1418,6 +1452,7 @@ function openItemSelect(type, workerIndex, taskIndex) {
|
||||
`).join('') : '<div style="text-align: center; padding: 2rem; color: #9ca3af;">선택 가능한 항목이 없습니다</div>';
|
||||
|
||||
modal.style.display = 'flex';
|
||||
lockBodyScroll();
|
||||
}
|
||||
window.openItemSelect = openItemSelect;
|
||||
|
||||
@@ -1449,6 +1484,7 @@ window.selectItem = selectItem;
|
||||
// 항목 선택 모달 닫기
|
||||
function closeItemSelectModal() {
|
||||
document.getElementById('itemSelectModal').style.display = 'none';
|
||||
unlockBodyScroll();
|
||||
currentEditingTaskLine = null;
|
||||
}
|
||||
window.closeItemSelectModal = closeItemSelectModal;
|
||||
@@ -1460,12 +1496,14 @@ async function openWorkplaceSelect(workerIndex, taskIndex) {
|
||||
currentEditingTaskLine = { workerIndex, taskIndex };
|
||||
await loadWorkplaceCategories();
|
||||
document.getElementById('workplaceSelectModal').style.display = 'flex';
|
||||
lockBodyScroll();
|
||||
}
|
||||
window.openWorkplaceSelect = openWorkplaceSelect;
|
||||
|
||||
// 작업장 선택 모달 닫기
|
||||
function closeWorkplaceSelectModal() {
|
||||
document.getElementById('workplaceSelectModal').style.display = 'none';
|
||||
unlockBodyScroll();
|
||||
document.getElementById('workplaceSelectionArea').style.display = 'none';
|
||||
document.getElementById('layoutMapArea').style.display = 'none';
|
||||
document.getElementById('workplaceList').style.display = 'none';
|
||||
@@ -1527,19 +1565,34 @@ async function selectCategory(categoryId, categoryName) {
|
||||
// 해당 카테고리 정보 가져오기
|
||||
const category = allWorkplaceCategories.find(c => c.category_id === categoryId);
|
||||
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
|
||||
// 지도 또는 리스트 로드
|
||||
if (category && category.layout_image) {
|
||||
// 지도가 있는 경우 - 지도 영역 표시
|
||||
// 지도가 있는 경우 - 지도를 기본 표시
|
||||
await loadWorkplaceMap(categoryId, category.layout_image);
|
||||
document.getElementById('layoutMapArea').style.display = 'block';
|
||||
|
||||
if (isMobile) {
|
||||
// 모바일: 리스트 숨기고 "리스트로 선택" 토글 표시
|
||||
document.getElementById('workplaceListSection').style.display = 'none';
|
||||
document.getElementById('toggleListBtn').style.display = 'inline-flex';
|
||||
document.getElementById('toggleListBtn').textContent = '리스트로 선택';
|
||||
} else {
|
||||
// 데스크톱: 리스트도 함께 표시
|
||||
document.getElementById('workplaceList').style.display = 'flex';
|
||||
document.getElementById('workplaceListSection').style.display = 'block';
|
||||
document.getElementById('toggleListBtn').style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// 지도가 없는 경우 - 리스트만 표시
|
||||
document.getElementById('layoutMapArea').style.display = 'none';
|
||||
document.getElementById('workplaceList').style.display = 'flex';
|
||||
document.getElementById('toggleListBtn').style.display = 'none';
|
||||
document.getElementById('workplaceList').style.display = 'flex';
|
||||
document.getElementById('workplaceListSection').style.display = 'block';
|
||||
}
|
||||
|
||||
// 해당 카테고리의 작업장 리스트 로드 (오류 대비용)
|
||||
// 해당 카테고리의 작업장 리스트 로드
|
||||
await loadWorkplacesByCategory(categoryId);
|
||||
|
||||
// 선택 완료 버튼 비활성화 (작업장 선택 필요)
|
||||
@@ -1638,22 +1691,18 @@ function confirmWorkplaceSelection() {
|
||||
}
|
||||
window.confirmWorkplaceSelection = confirmWorkplaceSelection;
|
||||
|
||||
// 리스트 토글 함수 (레거시 호환)
|
||||
// 리스트 토글 함수
|
||||
function toggleWorkplaceList() {
|
||||
const list = document.getElementById('workplaceList');
|
||||
const icon = document.getElementById('toggleListIcon');
|
||||
const listSection = document.getElementById('workplaceListSection');
|
||||
const btn = document.getElementById('toggleListBtn');
|
||||
|
||||
if (list.style.display === 'none' || list.style.display === '') {
|
||||
list.style.display = 'flex';
|
||||
icon.textContent = '▲';
|
||||
btn.textContent = ' 리스트 닫기';
|
||||
btn.insertBefore(icon, btn.firstChild);
|
||||
if (listSection.style.display === 'none') {
|
||||
listSection.style.display = 'block';
|
||||
document.getElementById('workplaceList').style.display = 'flex';
|
||||
btn.textContent = '리스트 숨기기';
|
||||
} else {
|
||||
list.style.display = 'none';
|
||||
icon.textContent = '▼';
|
||||
btn.textContent = ' 리스트 보기';
|
||||
btn.insertBefore(icon, btn.firstChild);
|
||||
listSection.style.display = 'none';
|
||||
btn.textContent = '리스트로 선택';
|
||||
}
|
||||
}
|
||||
window.toggleWorkplaceList = toggleWorkplaceList;
|
||||
@@ -1693,8 +1742,10 @@ async function loadWorkplaceMap(categoryId, layoutImagePath) {
|
||||
mapImage.crossOrigin = 'anonymous';
|
||||
|
||||
mapImage.onload = function() {
|
||||
// 캔버스 크기 설정 (최대 너비 800px)
|
||||
const maxWidth = 800;
|
||||
// 캔버스 크기 설정 (모바일 대응)
|
||||
const maxWidth = window.innerWidth <= 768
|
||||
? Math.min(window.innerWidth - 32, 600)
|
||||
: 800;
|
||||
const scale = mapImage.width > maxWidth ? maxWidth / mapImage.width : 1;
|
||||
|
||||
mapCanvas.width = mapImage.width * scale;
|
||||
@@ -1712,6 +1763,7 @@ async function loadWorkplaceMap(categoryId, layoutImagePath) {
|
||||
mapImage.onerror = function() {
|
||||
console.error('❌ 지도 이미지 로드 실패');
|
||||
document.getElementById('layoutMapArea').style.display = 'none';
|
||||
document.getElementById('workplaceListSection').style.display = 'block';
|
||||
document.getElementById('workplaceList').style.display = 'flex';
|
||||
document.getElementById('toggleListBtn').style.display = 'none';
|
||||
showToast('지도를 불러올 수 없어 리스트로 표시합니다.', 'warning');
|
||||
@@ -1779,8 +1831,12 @@ function handleMapClick(event) {
|
||||
if (!mapCanvas || mapRegions.length === 0) return;
|
||||
|
||||
const rect = mapCanvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
// CSS 스케일 보정: 캔버스의 논리적 크기와 화면 표시 크기가 다를 수 있음
|
||||
const scaleX = mapCanvas.width / rect.width;
|
||||
const scaleY = mapCanvas.height / rect.height;
|
||||
const x = (event.clientX - rect.left) * scaleX;
|
||||
const y = (event.clientY - rect.top) * scaleY;
|
||||
|
||||
// 클릭한 위치에 있는 영역 찾기
|
||||
for (let i = mapRegions.length - 1; i >= 0; i--) {
|
||||
@@ -1894,7 +1950,7 @@ async function openTeamCompositionModal(sessionId) {
|
||||
renderWorkerTaskList();
|
||||
|
||||
document.getElementById('tbmModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
lockBodyScroll();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 팀 구성 로드 오류:', error);
|
||||
@@ -1964,7 +2020,7 @@ window.deselectAllWorkers = deselectAllWorkers;
|
||||
// 팀 구성 모달 닫기
|
||||
function closeTeamModal() {
|
||||
document.getElementById('teamModal').style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
unlockBodyScroll();
|
||||
}
|
||||
window.closeTeamModal = closeTeamModal;
|
||||
|
||||
@@ -2094,7 +2150,7 @@ async function openSafetyCheckModal(sessionId) {
|
||||
|
||||
container.innerHTML = html;
|
||||
document.getElementById('safetyModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
lockBodyScroll();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 안전 체크 조회 오류:', error);
|
||||
@@ -2183,7 +2239,7 @@ function renderCheckItems(items) {
|
||||
// 안전 체크 모달 닫기
|
||||
function closeSafetyModal() {
|
||||
document.getElementById('safetyModal').style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
unlockBodyScroll();
|
||||
}
|
||||
window.closeSafetyModal = closeSafetyModal;
|
||||
|
||||
@@ -2226,14 +2282,14 @@ function openCompleteTbmModal(sessionId) {
|
||||
document.getElementById('endTime').value = timeString;
|
||||
|
||||
document.getElementById('completeModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
lockBodyScroll();
|
||||
}
|
||||
window.openCompleteTbmModal = openCompleteTbmModal;
|
||||
|
||||
// 완료 모달 닫기
|
||||
function closeCompleteModal() {
|
||||
document.getElementById('completeModal').style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
unlockBodyScroll();
|
||||
}
|
||||
window.closeCompleteModal = closeCompleteModal;
|
||||
|
||||
@@ -2289,46 +2345,86 @@ async function viewTbmSession(sessionId) {
|
||||
}
|
||||
|
||||
// 기본 정보 표시
|
||||
const leaderDisplay = session.leader_name || session.created_by_name || '-';
|
||||
const dateDisplay = formatDate(session.session_date) || '-';
|
||||
const statusMap = { draft: '진행중', completed: '완료', cancelled: '취소' };
|
||||
const statusText = statusMap[session.status] || session.status;
|
||||
|
||||
const basicInfo = document.getElementById('detailBasicInfo');
|
||||
basicInfo.innerHTML = `
|
||||
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem;">
|
||||
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">팀장</div>
|
||||
<div style="font-weight: 600; color: #111827;">${session.leader_name}</div>
|
||||
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">입력자</div>
|
||||
<div style="font-weight: 600; color: #111827;">${escapeHtml(leaderDisplay)}</div>
|
||||
</div>
|
||||
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem;">
|
||||
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">날짜</div>
|
||||
<div style="font-weight: 600; color: #111827;">${session.session_date}</div>
|
||||
<div style="font-weight: 600; color: #111827;">${escapeHtml(dateDisplay)}</div>
|
||||
</div>
|
||||
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem;">
|
||||
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">프로젝트</div>
|
||||
<div style="font-weight: 600; color: #111827;">${session.project_name || '-'}</div>
|
||||
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">상태</div>
|
||||
<div style="font-weight: 600; color: #111827;">${escapeHtml(statusText)}</div>
|
||||
</div>
|
||||
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem;">
|
||||
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">작업 장소</div>
|
||||
<div style="font-weight: 600; color: #111827;">${session.work_location || '-'}</div>
|
||||
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">팀원 수</div>
|
||||
<div style="font-weight: 600; color: #111827;">${parseInt(session.team_member_count) || team.length}명</div>
|
||||
</div>
|
||||
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem; grid-column: span 2;">
|
||||
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">작업 내용</div>
|
||||
<div style="color: #111827;">${session.work_description || '-'}</div>
|
||||
</div>
|
||||
${session.safety_notes ? `
|
||||
<div style="padding: 0.75rem; background: #fef3c7; border-radius: 0.375rem; grid-column: span 2;">
|
||||
<div style="font-size: 0.75rem; color: #92400e; margin-bottom: 0.25rem;">⚠️ 안전 특이사항</div>
|
||||
<div style="color: #78350f;">${session.safety_notes}</div>
|
||||
${session.project_name ? `
|
||||
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem;">
|
||||
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">프로젝트</div>
|
||||
<div style="font-weight: 600; color: #111827;">${escapeHtml(session.project_name)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${session.work_location ? `
|
||||
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem;">
|
||||
<div style="font-size: 0.75rem; color: #6b7280; margin-bottom: 0.25rem;">작업장</div>
|
||||
<div style="font-weight: 600; color: #111827;">${escapeHtml(session.work_location)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
// 팀 구성 표시
|
||||
const teamMembers = document.getElementById('detailTeamMembers');
|
||||
// 팀 구성 표시 (작업자별 작업 정보 포함)
|
||||
const teamContainer = document.getElementById('detailTeamMembers');
|
||||
if (team.length === 0) {
|
||||
teamMembers.innerHTML = '<p style="color: #6b7280; font-size: 0.875rem;">등록된 팀원이 없습니다.</p>';
|
||||
teamContainer.innerHTML = '<p style="color: #6b7280; font-size: 0.875rem;">등록된 팀원이 없습니다.</p>';
|
||||
} else {
|
||||
teamMembers.innerHTML = team.map(member => `
|
||||
<div style="padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem; border: 1px solid #e5e7eb;">
|
||||
<div style="font-weight: 600; color: #111827; margin-bottom: 0.25rem;">${member.worker_name}</div>
|
||||
<div style="font-size: 0.75rem; color: #6b7280;">${member.job_type || ''}</div>
|
||||
${member.is_present ? '' : '<div style="font-size: 0.75rem; color: #ef4444; margin-top: 0.25rem;">결석</div>'}
|
||||
// 작업자별로 그룹화
|
||||
const workerMap = new Map();
|
||||
team.forEach(member => {
|
||||
if (!workerMap.has(member.worker_id)) {
|
||||
workerMap.set(member.worker_id, {
|
||||
worker_name: member.worker_name,
|
||||
job_type: member.job_type,
|
||||
is_present: member.is_present,
|
||||
tasks: []
|
||||
});
|
||||
}
|
||||
workerMap.get(member.worker_id).tasks.push(member);
|
||||
});
|
||||
|
||||
teamContainer.style.display = 'flex';
|
||||
teamContainer.style.flexDirection = 'column';
|
||||
teamContainer.style.gap = '0.75rem';
|
||||
teamContainer.style.gridTemplateColumns = '';
|
||||
|
||||
teamContainer.innerHTML = Array.from(workerMap.values()).map(worker => `
|
||||
<div style="border: 1px solid #e5e7eb; border-radius: 0.5rem; overflow: hidden;">
|
||||
<div style="padding: 0.625rem 0.875rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; display: flex; align-items: center; justify-content: space-between;">
|
||||
<div>
|
||||
<span style="font-weight: 600;">${escapeHtml(worker.worker_name)}</span>
|
||||
<span style="font-size: 0.75rem; opacity: 0.85; margin-left: 0.25rem;">${escapeHtml(worker.job_type || '')}</span>
|
||||
</div>
|
||||
${!worker.is_present ? '<span style="font-size: 0.75rem; background: rgba(239,68,68,0.8); padding: 0.125rem 0.5rem; border-radius: 4px;">결석</span>' : ''}
|
||||
</div>
|
||||
<div style="padding: 0.625rem 0.875rem;">
|
||||
${worker.tasks.map(t => `
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.375rem; margin-bottom: 0.375rem;">
|
||||
${t.project_name ? `<span style="font-size: 0.75rem; padding: 0.125rem 0.5rem; background: #dbeafe; color: #1e40af; border-radius: 4px;">${escapeHtml(t.project_name)}</span>` : ''}
|
||||
${t.work_type_name ? `<span style="font-size: 0.75rem; padding: 0.125rem 0.5rem; background: #fef3c7; color: #92400e; border-radius: 4px;">${escapeHtml(t.work_type_name)}</span>` : ''}
|
||||
${t.task_name ? `<span style="font-size: 0.75rem; padding: 0.125rem 0.5rem; background: #dcfce7; color: #166534; border-radius: 4px;">${escapeHtml(t.task_name)}</span>` : ''}
|
||||
${t.workplace_name ? `<span style="font-size: 0.75rem; padding: 0.125rem 0.5rem; background: #f1f5f9; color: #475569; border-radius: 4px;">${escapeHtml((t.workplace_category_name ? t.workplace_category_name + ' > ' : '') + t.workplace_name)}</span>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
@@ -2371,8 +2467,28 @@ async function viewTbmSession(sessionId) {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 푸터 버튼 동적 생성
|
||||
const footer = document.getElementById('detailModalFooter');
|
||||
const safeId = parseInt(session.session_id) || 0;
|
||||
console.log('📋 TBM 상세 - session_id:', safeId, 'status:', session.status);
|
||||
if (session.status === 'draft') {
|
||||
footer.innerHTML = `
|
||||
<button type="button" class="tbm-btn tbm-btn-danger" onclick="confirmDeleteTbm(${safeId})">
|
||||
삭제
|
||||
</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="closeDetailModal(); openTeamCompositionModal(${safeId})">
|
||||
수정
|
||||
</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeDetailModal()">닫기</button>
|
||||
`;
|
||||
} else {
|
||||
footer.innerHTML = `
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeDetailModal()">닫기</button>
|
||||
`;
|
||||
}
|
||||
|
||||
document.getElementById('detailModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
lockBodyScroll();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ TBM 상세 조회 오류:', error);
|
||||
@@ -2381,10 +2497,42 @@ async function viewTbmSession(sessionId) {
|
||||
}
|
||||
window.viewTbmSession = viewTbmSession;
|
||||
|
||||
// TBM 삭제 확인
|
||||
function confirmDeleteTbm(sessionId) {
|
||||
if (!confirm('이 TBM을 삭제하시겠습니까?\n삭제 후 복구할 수 없습니다.')) return;
|
||||
deleteTbmSession(sessionId);
|
||||
}
|
||||
window.confirmDeleteTbm = confirmDeleteTbm;
|
||||
|
||||
// TBM 세션 삭제
|
||||
async function deleteTbmSession(sessionId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/${sessionId}`, 'DELETE');
|
||||
|
||||
if (response && response.success) {
|
||||
showToast('TBM이 삭제되었습니다.', 'success');
|
||||
closeDetailModal();
|
||||
|
||||
// 목록 새로고침
|
||||
if (currentTab === 'tbm-input') {
|
||||
await loadTodayOnlyTbm();
|
||||
} else {
|
||||
await loadRecentTbmGroupedByDate();
|
||||
}
|
||||
} else {
|
||||
showToast(response?.message || 'TBM 삭제에 실패했습니다.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ TBM 삭제 오류:', error);
|
||||
showToast('TBM 삭제 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
window.deleteTbmSession = deleteTbmSession;
|
||||
|
||||
// 상세보기 모달 닫기
|
||||
function closeDetailModal() {
|
||||
document.getElementById('detailModal').style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
unlockBodyScroll();
|
||||
}
|
||||
window.closeDetailModal = closeDetailModal;
|
||||
|
||||
@@ -2448,7 +2596,7 @@ async function openHandoverModal(sessionId) {
|
||||
document.getElementById('handoverNotes').value = '';
|
||||
|
||||
document.getElementById('handoverModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
lockBodyScroll();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 인계 모달 열기 오류:', error);
|
||||
@@ -2460,7 +2608,7 @@ window.openHandoverModal = openHandoverModal;
|
||||
// 인계 모달 닫기
|
||||
function closeHandoverModal() {
|
||||
document.getElementById('handoverModal').style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
unlockBodyScroll();
|
||||
}
|
||||
window.closeHandoverModal = closeHandoverModal;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user