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:
@@ -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