refactor: TBM 페이지를 탭 기반 UI로 개선
- TBM 입력 탭: 오늘의 TBM 목록 + 새 TBM 시작 버튼 - TBM 관리 탭: 전체 TBM 기록 + 날짜 필터링 - 탭 전환 로직 추가 - 각 탭별 통계 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
264
web-ui/js/tbm.js
264
web-ui/js/tbm.js
@@ -2,11 +2,13 @@
|
||||
|
||||
// 전역 변수
|
||||
let allSessions = [];
|
||||
let todaySessions = [];
|
||||
let allWorkers = [];
|
||||
let allProjects = [];
|
||||
let allSafetyChecks = [];
|
||||
let currentSessionId = null;
|
||||
let selectedWorkers = new Set();
|
||||
let currentTab = 'tbm-input';
|
||||
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
@@ -34,7 +36,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// 초기 데이터 로드
|
||||
await loadInitialData();
|
||||
await loadTodayTbm();
|
||||
await loadTodayOnlyTbm();
|
||||
});
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
@@ -81,7 +83,98 @@ async function loadInitialData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 오늘 TBM 로드
|
||||
// ==================== 탭 전환 ====================
|
||||
|
||||
// 탭 전환
|
||||
function switchTbmTab(tabName) {
|
||||
currentTab = tabName;
|
||||
|
||||
// 탭 버튼 활성화 상태 변경
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
if (btn.dataset.tab === tabName) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 탭 컨텐츠 표시 변경
|
||||
document.querySelectorAll('.code-tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`${tabName}-tab`).classList.add('active');
|
||||
|
||||
// 탭에 따라 데이터 로드
|
||||
if (tabName === 'tbm-input') {
|
||||
loadTodayOnlyTbm();
|
||||
} else if (tabName === 'tbm-manage') {
|
||||
const tbmDate = document.getElementById('tbmDate');
|
||||
if (tbmDate && tbmDate.value) {
|
||||
loadTbmSessionsByDate(tbmDate.value);
|
||||
} else {
|
||||
loadTodayTbm();
|
||||
}
|
||||
}
|
||||
}
|
||||
window.switchTbmTab = switchTbmTab;
|
||||
|
||||
// ==================== TBM 입력 탭 ====================
|
||||
|
||||
// 오늘의 TBM만 로드 (TBM 입력 탭용)
|
||||
async function loadTodayOnlyTbm() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(`/tbm/sessions/date/${today}`);
|
||||
|
||||
if (response && response.success) {
|
||||
todaySessions = response.data || [];
|
||||
displayTodayTbmSessions();
|
||||
} else {
|
||||
todaySessions = [];
|
||||
displayTodayTbmSessions();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 오늘 TBM 조회 오류:', error);
|
||||
showToast('오늘 TBM을 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
todaySessions = [];
|
||||
displayTodayTbmSessions();
|
||||
}
|
||||
}
|
||||
window.loadTodayOnlyTbm = loadTodayOnlyTbm;
|
||||
|
||||
// 오늘의 TBM 세션 표시
|
||||
function displayTodayTbmSessions() {
|
||||
const grid = document.getElementById('todayTbmGrid');
|
||||
const emptyState = document.getElementById('todayEmptyState');
|
||||
const todayTotalEl = document.getElementById('todayTotalSessions');
|
||||
const todayCompletedEl = document.getElementById('todayCompletedSessions');
|
||||
const todayActiveEl = document.getElementById('todayActiveSessions');
|
||||
|
||||
if (todaySessions.length === 0) {
|
||||
grid.innerHTML = '';
|
||||
emptyState.style.display = 'flex';
|
||||
todayTotalEl.textContent = '0';
|
||||
todayCompletedEl.textContent = '0';
|
||||
todayActiveEl.textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
const completedCount = todaySessions.filter(s => s.status === 'completed').length;
|
||||
const activeCount = todaySessions.filter(s => s.status === 'draft').length;
|
||||
|
||||
todayTotalEl.textContent = todaySessions.length;
|
||||
todayCompletedEl.textContent = completedCount;
|
||||
todayActiveEl.textContent = activeCount;
|
||||
|
||||
grid.innerHTML = todaySessions.map(session => createSessionCard(session)).join('');
|
||||
}
|
||||
|
||||
// ==================== TBM 관리 탭 ====================
|
||||
|
||||
// 오늘 TBM 로드 (TBM 관리 탭용)
|
||||
async function loadTodayTbm() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('tbmDate').value = today;
|
||||
@@ -89,6 +182,28 @@ async function loadTodayTbm() {
|
||||
}
|
||||
window.loadTodayTbm = loadTodayTbm;
|
||||
|
||||
// 전체 TBM 로드
|
||||
async function loadAllTbm() {
|
||||
try {
|
||||
const response = await window.apiCall('/tbm/sessions');
|
||||
|
||||
if (response && response.success) {
|
||||
allSessions = response.data || [];
|
||||
document.getElementById('tbmDate').value = '';
|
||||
displayTbmSessions();
|
||||
} else {
|
||||
allSessions = [];
|
||||
displayTbmSessions();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 전체 TBM 조회 오류:', error);
|
||||
showToast('전체 TBM을 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
allSessions = [];
|
||||
displayTbmSessions();
|
||||
}
|
||||
}
|
||||
window.loadAllTbm = loadAllTbm;
|
||||
|
||||
// 특정 날짜의 TBM 세션 목록 로드
|
||||
async function loadTbmSessionsByDate(date) {
|
||||
try {
|
||||
@@ -109,7 +224,7 @@ async function loadTbmSessionsByDate(date) {
|
||||
}
|
||||
}
|
||||
|
||||
// TBM 세션 목록 표시
|
||||
// TBM 세션 목록 표시 (관리 탭용)
|
||||
function displayTbmSessions() {
|
||||
const grid = document.getElementById('tbmSessionsGrid');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
@@ -130,71 +245,74 @@ function displayTbmSessions() {
|
||||
totalSessionsEl.textContent = allSessions.length;
|
||||
completedSessionsEl.textContent = completedCount;
|
||||
|
||||
grid.innerHTML = allSessions.map(session => {
|
||||
const statusBadge = {
|
||||
'draft': '<span class="badge" style="background: #fef3c7; color: #92400e;">진행중</span>',
|
||||
'completed': '<span class="badge" style="background: #dcfce7; color: #166534;">완료</span>',
|
||||
'cancelled': '<span class="badge" style="background: #fee2e2; color: #991b1b;">취소</span>'
|
||||
}[session.status] || '';
|
||||
grid.innerHTML = allSessions.map(session => createSessionCard(session)).join('');
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="project-card" style="cursor: pointer;" onclick="viewTbmSession(${session.session_id})">
|
||||
<div class="project-header">
|
||||
<div>
|
||||
<h3 class="project-name" style="font-size: 1rem; margin-bottom: 0.25rem;">
|
||||
${session.leader_name || '팀장 미지정'}
|
||||
</h3>
|
||||
<p style="font-size: 0.75rem; color: #6b7280; margin: 0;">
|
||||
${session.leader_job_type || ''}
|
||||
</p>
|
||||
</div>
|
||||
${statusBadge}
|
||||
// TBM 세션 카드 생성 (공통)
|
||||
function createSessionCard(session) {
|
||||
const statusBadge = {
|
||||
'draft': '<span class="badge" style="background: #fef3c7; color: #92400e;">진행중</span>',
|
||||
'completed': '<span class="badge" style="background: #dcfce7; color: #166534;">완료</span>',
|
||||
'cancelled': '<span class="badge" style="background: #fee2e2; color: #991b1b;">취소</span>'
|
||||
}[session.status] || '';
|
||||
|
||||
return `
|
||||
<div class="project-card" style="cursor: pointer;" onclick="viewTbmSession(${session.session_id})">
|
||||
<div class="project-header">
|
||||
<div>
|
||||
<h3 class="project-name" style="font-size: 1rem; margin-bottom: 0.25rem;">
|
||||
${session.leader_name || '팀장 미지정'}
|
||||
</h3>
|
||||
<p style="font-size: 0.75rem; color: #6b7280; margin: 0;">
|
||||
${session.session_date} | ${session.leader_job_type || ''}
|
||||
</p>
|
||||
</div>
|
||||
${statusBadge}
|
||||
</div>
|
||||
|
||||
<div class="project-info" style="margin-top: 1rem;">
|
||||
<div class="info-item">
|
||||
<span class="info-label">프로젝트</span>
|
||||
<span class="info-value">${session.project_name || '-'}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">작업 장소</span>
|
||||
<span class="info-value">${session.work_location || '-'}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">팀원 수</span>
|
||||
<span class="info-value">${session.team_member_count || 0}명</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">시작 시간</span>
|
||||
<span class="info-value">${session.start_time || '-'}</span>
|
||||
</div>
|
||||
<div class="project-info" style="margin-top: 1rem;">
|
||||
<div class="info-item">
|
||||
<span class="info-label">프로젝트</span>
|
||||
<span class="info-value">${session.project_name || '-'}</span>
|
||||
</div>
|
||||
|
||||
${session.work_description ? `
|
||||
<div style="margin-top: 0.75rem; padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem; font-size: 0.875rem; color: #374151;">
|
||||
${session.work_description}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div style="margin-top: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
${session.status === 'draft' ? `
|
||||
<button class="btn btn-sm btn-primary" onclick="event.stopPropagation(); openTeamCompositionModal(${session.session_id})" style="flex: 1; min-width: 100px;">
|
||||
👥 팀 구성
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); openSafetyCheckModal(${session.session_id})" style="flex: 1; min-width: 100px;">
|
||||
✅ 안전 체크
|
||||
</button>
|
||||
<button class="btn btn-sm" style="background: #f59e0b; color: white; border: none;" onclick="event.stopPropagation(); openHandoverModal(${session.session_id})">
|
||||
📤 인계
|
||||
</button>
|
||||
<button class="btn btn-sm btn-success" onclick="event.stopPropagation(); openCompleteTbmModal(${session.session_id})">
|
||||
완료
|
||||
</button>
|
||||
` : ''}
|
||||
<div class="info-item">
|
||||
<span class="info-label">작업 장소</span>
|
||||
<span class="info-value">${session.work_location || '-'}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">팀원 수</span>
|
||||
<span class="info-value">${session.team_member_count || 0}명</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">시작 시간</span>
|
||||
<span class="info-value">${session.start_time || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
${session.work_description ? `
|
||||
<div style="margin-top: 0.75rem; padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem; font-size: 0.875rem; color: #374151;">
|
||||
${session.work_description}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div style="margin-top: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
${session.status === 'draft' ? `
|
||||
<button class="btn btn-sm btn-primary" onclick="event.stopPropagation(); openTeamCompositionModal(${session.session_id})" style="flex: 1; min-width: 100px;">
|
||||
👥 팀 구성
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); openSafetyCheckModal(${session.session_id})" style="flex: 1; min-width: 100px;">
|
||||
✅ 안전 체크
|
||||
</button>
|
||||
<button class="btn btn-sm" style="background: #f59e0b; color: white; border: none;" onclick="event.stopPropagation(); openHandoverModal(${session.session_id})">
|
||||
📤 인계
|
||||
</button>
|
||||
<button class="btn btn-sm btn-success" onclick="event.stopPropagation(); openCompleteTbmModal(${session.session_id})">
|
||||
완료
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 새 TBM 모달 열기
|
||||
@@ -276,7 +394,11 @@ async function saveTbmSession() {
|
||||
const createdSessionId = response.data.session_id;
|
||||
|
||||
// 목록 새로고침
|
||||
await loadTbmSessionsByDate(sessionData.session_date);
|
||||
if (currentTab === 'tbm-input') {
|
||||
await loadTodayOnlyTbm();
|
||||
} else {
|
||||
await loadTbmSessionsByDate(sessionData.session_date);
|
||||
}
|
||||
|
||||
// 팀 구성 모달 열기
|
||||
setTimeout(() => {
|
||||
@@ -423,8 +545,12 @@ async function saveTeamComposition() {
|
||||
closeTeamModal();
|
||||
|
||||
// 목록 새로고침
|
||||
const date = document.getElementById('tbmDate').value;
|
||||
await loadTbmSessionsByDate(date);
|
||||
if (currentTab === 'tbm-input') {
|
||||
await loadTodayOnlyTbm();
|
||||
} else {
|
||||
const date = document.getElementById('tbmDate').value;
|
||||
await loadTbmSessionsByDate(date);
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.message || '저장에 실패했습니다.');
|
||||
}
|
||||
@@ -577,8 +703,12 @@ async function completeTbmSession() {
|
||||
closeCompleteModal();
|
||||
|
||||
// 목록 새로고침
|
||||
const date = document.getElementById('tbmDate').value;
|
||||
await loadTbmSessionsByDate(date);
|
||||
if (currentTab === 'tbm-input') {
|
||||
await loadTodayOnlyTbm();
|
||||
} else {
|
||||
const date = document.getElementById('tbmDate').value;
|
||||
await loadTbmSessionsByDate(date);
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.message || '완료 처리에 실패했습니다.');
|
||||
}
|
||||
|
||||
@@ -23,52 +23,117 @@
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">
|
||||
<span class="title-icon">🛠️</span>
|
||||
TBM (Tool Box Meeting) 관리
|
||||
TBM (Tool Box Meeting)
|
||||
</h1>
|
||||
<p class="page-description">아침 안전 회의 및 팀 구성 관리</p>
|
||||
</div>
|
||||
|
||||
<div class="page-actions">
|
||||
<input type="date" id="tbmDate" class="form-control" style="display: inline-block; width: auto;">
|
||||
<button class="btn btn-secondary" onclick="loadTodayTbm()">
|
||||
<span class="btn-icon">📅</span>
|
||||
오늘
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="openNewTbmModal()">
|
||||
<span class="btn-icon">➕</span>
|
||||
새 TBM 시작
|
||||
</button>
|
||||
<div class="page-actions" id="headerActions">
|
||||
<!-- 탭에 따라 동적으로 변경됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TBM 세션 목록 -->
|
||||
<div class="projects-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">TBM 세션 목록</h2>
|
||||
<div class="project-stats">
|
||||
<!-- TBM 탭 -->
|
||||
<div class="code-tabs">
|
||||
<button class="tab-btn active" data-tab="tbm-input" onclick="switchTbmTab('tbm-input')">
|
||||
<span class="tab-icon">➕</span>
|
||||
TBM 입력
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="tbm-manage" onclick="switchTbmTab('tbm-manage')">
|
||||
<span class="tab-icon">📋</span>
|
||||
TBM 관리
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- TBM 입력 탭 -->
|
||||
<div id="tbm-input-tab" class="code-tab-content active">
|
||||
<div class="code-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<span class="section-icon">🌅</span>
|
||||
오늘의 TBM
|
||||
</h2>
|
||||
<div class="section-actions">
|
||||
<button class="btn btn-primary" onclick="openNewTbmModal()">
|
||||
<span class="btn-icon">➕</span>
|
||||
새 TBM 시작
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-stats">
|
||||
<span class="stat-item">
|
||||
<span class="stat-icon">📋</span>
|
||||
오늘 등록 <span id="todayTotalSessions">0</span>개
|
||||
</span>
|
||||
<span class="stat-item">
|
||||
<span class="stat-icon">✅</span>
|
||||
완료 <span id="todayCompletedSessions">0</span>개
|
||||
</span>
|
||||
<span class="stat-item">
|
||||
<span class="stat-icon">⏳</span>
|
||||
진행중 <span id="todayActiveSessions">0</span>개
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="code-grid" id="todayTbmGrid">
|
||||
<!-- 오늘의 TBM 세션 카드들이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state" id="todayEmptyState" style="display: none;">
|
||||
<div class="empty-icon">🛠️</div>
|
||||
<h3>오늘 등록된 TBM이 없습니다</h3>
|
||||
<p>"새 TBM 시작" 버튼을 눌러 오늘의 TBM을 시작해보세요.</p>
|
||||
<button class="btn btn-primary" onclick="openNewTbmModal()">
|
||||
➕ 첫 TBM 시작하기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TBM 관리 탭 -->
|
||||
<div id="tbm-manage-tab" class="code-tab-content">
|
||||
<div class="code-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<span class="section-icon">📚</span>
|
||||
전체 TBM 기록
|
||||
</h2>
|
||||
<div class="section-actions">
|
||||
<input type="date" id="tbmDate" class="form-control" style="display: inline-block; width: auto; margin-right: 0.5rem;">
|
||||
<button class="btn btn-secondary" onclick="loadTodayTbm()">
|
||||
<span class="btn-icon">📅</span>
|
||||
오늘
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="loadAllTbm()">
|
||||
<span class="btn-icon">🔄</span>
|
||||
전체 보기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-stats">
|
||||
<span class="stat-item">
|
||||
<span class="stat-icon">📋</span>
|
||||
총 <span id="totalSessions">0</span>개
|
||||
</span>
|
||||
<span class="stat-item" style="color: #16a34a;">
|
||||
<span class="stat-item">
|
||||
<span class="stat-icon">✅</span>
|
||||
완료 <span id="completedSessions">0</span>개
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="projects-grid" id="tbmSessionsGrid">
|
||||
<!-- TBM 세션 카드들이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
<div class="code-grid" id="tbmSessionsGrid">
|
||||
<!-- 전체 TBM 세션 카드들이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state" id="emptyState" style="display: none;">
|
||||
<div class="empty-icon">🛠️</div>
|
||||
<h3>등록된 TBM 세션이 없습니다.</h3>
|
||||
<p>\"새 TBM 시작\" 버튼을 눌러 오늘의 TBM을 시작해보세요.</p>
|
||||
<button class="btn btn-primary" onclick="openNewTbmModal()">
|
||||
➕ 첫 TBM 시작하기
|
||||
</button>
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state" id="emptyState" style="display: none;">
|
||||
<div class="empty-icon">🛠️</div>
|
||||
<h3>등록된 TBM 세션이 없습니다</h3>
|
||||
<p>TBM 입력 탭에서 새로운 TBM을 시작해보세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user