feat: 일일순회점검 시스템 구축 및 관리 기능 개선

- 일일순회점검 시스템 신규 구현
  - DB 테이블: patrol_checklist_items, daily_patrol_sessions, patrol_check_records, workplace_items, item_types
  - API: /api/patrol/* 엔드포인트
  - 프론트엔드: 지도 기반 작업장 점검 UI

- 설비 관리 기능 개선
  - 구매 관련 필드 추가 (구매일, 가격, 공급업체 등)
  - 설비 코드 자동 생성 (TKP-XXX 형식)

- 작업장 관리 개선
  - 레이아웃 이미지 업로드 기능
  - 마커 위치 저장 기능

- 부서 관리 기능 추가
- 사이드바 네비게이션 카테고리 재구성
- 이미지 401 오류 수정 (정적 파일 경로 처리)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-04 11:41:41 +09:00
parent 2e9d24faf2
commit 90d3e32992
101 changed files with 17421 additions and 7047 deletions

View File

@@ -118,8 +118,8 @@ async function loadInitialData() {
currentUser = userInfo;
console.log('👤 로그인 사용자:', currentUser, 'worker_id:', currentUser?.worker_id);
// 작업자 목록 로드
const workersResponse = await window.apiCall('/workers?limit=1000');
// 작업자 목록 로드 (생산팀 소속만)
const workersResponse = await window.apiCall('/workers?limit=1000&department_id=1');
if (workersResponse) {
allWorkers = Array.isArray(workersResponse) ? workersResponse : (workersResponse.data || []);
// 활성 상태인 작업자만 필터링
@@ -185,7 +185,7 @@ function switchTbmTab(tabName) {
currentTab = tabName;
// 탭 버튼 활성화 상태 변경
document.querySelectorAll('.tab-btn').forEach(btn => {
document.querySelectorAll('.tbm-tab-btn').forEach(btn => {
if (btn.dataset.tab === tabName) {
btn.classList.add('active');
} else {
@@ -194,7 +194,7 @@ function switchTbmTab(tabName) {
});
// 탭 컨텐츠 표시 변경
document.querySelectorAll('.code-tab-content').forEach(content => {
document.querySelectorAll('.tbm-tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabName}-tab`).classList.add('active');
@@ -409,20 +409,33 @@ function displayTbmGroupedByDate() {
const displayDate = `${parseInt(month)}${parseInt(day)}`;
return `
<div class="date-group">
<div class="date-group-header ${isToday ? 'today' : ''}">
<span class="date-group-date">${displayDate}</span>
<span class="date-group-day">${dayName}요일${isToday ? ' (오늘)' : ''}</span>
<span class="date-group-count">${sessions.length}</span>
<div class="tbm-date-group" data-date="${date}">
<div class="tbm-date-header ${isToday ? 'today' : ''}" onclick="toggleDateGroup('${date}')">
<span class="tbm-date-toggle">&#9660;</span>
<span class="tbm-date-title">${displayDate}</span>
<span class="tbm-date-day">${dayName}요일</span>
${isToday ? '<span class="tbm-today-badge">오늘</span>' : ''}
<span class="tbm-date-count">${sessions.length}건</span>
</div>
<div class="date-group-grid">
${sessions.map(session => createSessionCard(session)).join('')}
<div class="tbm-date-content">
<div class="tbm-date-grid">
${sessions.map(session => createSessionCard(session)).join('')}
</div>
</div>
</div>
`;
}).join('');
}
// 날짜 그룹 토글
function toggleDateGroup(date) {
const group = document.querySelector(`.tbm-date-group[data-date="${date}"]`);
if (group) {
group.classList.toggle('collapsed');
}
}
window.toggleDateGroup = toggleDateGroup;
/**
* 더 많은 날짜 로드
*/
@@ -474,73 +487,66 @@ function displayTbmSessions() {
// 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>'
'draft': '<span class="tbm-card-status draft">진행중</span>',
'completed': '<span class="tbm-card-status completed">완료</span>',
'cancelled': '<span class="tbm-card-status cancelled">취소</span>'
}[session.status] || '';
// 작업 책임자 표시 (leader_name이 있으면 표시, 없으면 created_by_name 표시)
const leaderDisplay = session.leader_name
? `${session.leader_name} (${session.leader_job_type || '작업자'})`
: `${session.created_by_name || '작업 책임자'} (관리자)`;
const leaderName = session.leader_name || session.created_by_name || '작업 책임자';
const leaderRole = session.leader_name
? (session.leader_job_type || '작업자')
: '관리자';
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;">
${leaderDisplay}
</h3>
<p style="font-size: 0.75rem; color: #6b7280; margin: 0;">
${formatDate(session.session_date)}
</p>
<div class="tbm-session-card" onclick="viewTbmSession(${session.session_id})">
<div class="tbm-card-header">
<div class="tbm-card-header-top">
<div>
<h3 class="tbm-card-leader">
${leaderName}
<span class="tbm-card-leader-role">${leaderRole}</span>
</h3>
</div>
${statusBadge}
</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_type_name || '-'}</span>
</div>
<div class="info-item">
<span class="info-label">작업</span>
<span class="info-value">${session.task_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 class="tbm-card-date">
<span>&#128197;</span>
${formatDate(session.session_date)} ${session.start_time ? '| ' + session.start_time : ''}
</div>
</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 class="tbm-card-body">
<div class="tbm-card-info-grid">
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">프로젝트</span>
<span class="tbm-card-info-value">${session.project_name || '-'}</span>
</div>
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">공정</span>
<span class="tbm-card-info-value">${session.work_type_name || '-'}</span>
</div>
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">작업장</span>
<span class="tbm-card-info-value">${session.work_location || '-'}</span>
</div>
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">팀원</span>
<span class="tbm-card-info-value">${session.team_member_count || 0}명</span>
</div>
</div>
</div>
${session.status === 'draft' ? `
<div class="tbm-card-footer">
<button class="tbm-btn tbm-btn-primary tbm-btn-sm" onclick="event.stopPropagation(); openTeamCompositionModal(${session.session_id})">
&#128101; 팀 구성
</button>
<button class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="event.stopPropagation(); openSafetyCheckModal(${session.session_id})">
&#10003; 안전 체크
</button>
</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>
` : ''}
</div>
</div>
`;
}
@@ -550,23 +556,33 @@ function openNewTbmModal() {
currentSessionId = null;
workerTaskList = []; // 작업자 목록 초기화
document.getElementById('modalTitle').textContent = '새 TBM 시작';
document.getElementById('modalTitle').innerHTML = '<span>&#128221;</span> 새 TBM 시작';
document.getElementById('sessionId').value = '';
document.getElementById('tbmForm').reset();
const today = getTodayKST();
document.getElementById('sessionDate').value = today;
// 날짜 표시 업데이트
const [year, month, day] = today.split('-');
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
const dateObj = new Date(today);
const dayName = dayNames[dateObj.getDay()];
const sessionDateDisplay = document.getElementById('sessionDateDisplay');
if (sessionDateDisplay) {
sessionDateDisplay.textContent = `${year}${parseInt(month)}${parseInt(day)}일 (${dayName})`;
}
// 입력자 자동 설정 (readonly)
if (currentUser && currentUser.worker_id) {
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
if (worker) {
document.getElementById('leaderName').value = worker.worker_name;
document.getElementById('leaderName').textContent = worker.worker_name;
document.getElementById('leaderId').value = worker.worker_id;
}
} else if (currentUser && currentUser.name) {
// 관리자: 이름만 표시
document.getElementById('leaderName').value = currentUser.name;
document.getElementById('leaderName').textContent = currentUser.name;
document.getElementById('leaderId').value = '';
}