feat: 임시 이동 설비 현황 표시 기능 추가
- 대시보드 하단에 임시 이동된 설비 카드 섹션 추가 - 작업장 모달에 '이동 설비' 탭 추가 - 이 작업장으로 이동해 온 설비 표시 - 다른 곳으로 이동한 설비 표시 - 설비 마커에 이동 상태 색상 구분 (주황색 점선 + 깜빡임) - 원위치 복귀 기능 - 사이드바 기본값을 접힌 상태로 변경 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2940,6 +2940,23 @@
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* 임시 이동된 설비 */
|
||||
.equipment-marker.moved {
|
||||
border-color: #f59e0b;
|
||||
border-style: dashed;
|
||||
border-width: 3px;
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
animation: movedPulse 2s infinite;
|
||||
}
|
||||
.equipment-marker.moved .marker-label {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
@keyframes movedPulse {
|
||||
0%, 100% { border-color: #f59e0b; }
|
||||
50% { border-color: #fbbf24; }
|
||||
}
|
||||
|
||||
@keyframes pulse-border {
|
||||
0%, 100% { border-color: #dc2626; }
|
||||
50% { border-color: #fca5a5; }
|
||||
@@ -3825,3 +3842,168 @@
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 임시 이동 설비 목록 ==================== */
|
||||
.moved-equipment-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.moved-equipment-card {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.moved-equipment-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.moved-eq-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.moved-eq-code {
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.moved-eq-badge {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.moved-eq-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.moved-eq-location {
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.location-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.location-label {
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.location-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.location-value.current {
|
||||
color: #f59e0b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.location-arrow {
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
font-size: 1rem;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.moved-eq-date {
|
||||
font-size: 0.8rem;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.moved-equipment-card .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 모달 내 이동 설비 탭 스타일 */
|
||||
.moved-eq-tab-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.moved-eq-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.moved-eq-item {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
border-left: 4px solid #9ca3af;
|
||||
}
|
||||
|
||||
.moved-eq-item.in {
|
||||
border-left-color: #22c55e;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.moved-eq-item.out {
|
||||
border-left-color: #f59e0b;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.moved-eq-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.moved-eq-item-header .eq-code {
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.moved-eq-item-header .eq-name {
|
||||
font-size: 0.9rem;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.moved-eq-item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.moved-eq-item-info .arrow {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.moved-eq-item-info .to-location {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.moved-eq-item-date {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@@ -201,8 +201,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 저장된 상태 복원
|
||||
const isCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
|
||||
// 저장된 상태 복원 (기본값: 접힌 상태)
|
||||
const isCollapsed = localStorage.getItem('sidebarCollapsed') !== 'false';
|
||||
const sidebar = doc.querySelector('.sidebar-nav');
|
||||
if (isCollapsed && sidebar) {
|
||||
sidebar.classList.add('collapsed');
|
||||
|
||||
@@ -116,10 +116,10 @@ function highlightCurrentPage(doc) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 사이드바 상태 복원
|
||||
* 사이드바 상태 복원 (기본값: 접힌 상태)
|
||||
*/
|
||||
function restoreSidebarState(doc) {
|
||||
const isCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
|
||||
const isCollapsed = localStorage.getItem('sidebarCollapsed') !== 'false';
|
||||
const sidebar = doc.querySelector('.sidebar-nav');
|
||||
|
||||
if (isCollapsed && sidebar) {
|
||||
|
||||
@@ -24,6 +24,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// 기본값으로 제1공장 선택
|
||||
await selectFirstCategory();
|
||||
|
||||
// 임시 이동된 설비 로드
|
||||
await loadMovedEquipments();
|
||||
});
|
||||
|
||||
// ==================== 카테고리 (공장) 로드 ====================
|
||||
@@ -641,9 +644,15 @@ async function loadEquipmentMarkers(workplaceId) {
|
||||
equipments.forEach(eq => {
|
||||
// 위치 정보가 있는 설비만 마커 표시
|
||||
if (eq.map_x_percent != null && eq.map_y_percent != null) {
|
||||
const statusClass = eq.status === 'active' ? 'active' :
|
||||
eq.status === 'maintenance' ? 'maintenance' :
|
||||
eq.status === 'repair_needed' ? 'repair' : 'inactive';
|
||||
// 임시 이동된 설비는 별도 클래스 적용
|
||||
let statusClass = '';
|
||||
if (eq.is_temporarily_moved) {
|
||||
statusClass = 'moved';
|
||||
} else {
|
||||
statusClass = eq.status === 'active' ? 'active' :
|
||||
eq.status === 'maintenance' ? 'maintenance' :
|
||||
eq.status === 'repair_needed' ? 'repair' : 'inactive';
|
||||
}
|
||||
|
||||
// 마커 크기 (기본값 또는 설정된 값)
|
||||
const width = eq.map_width_percent || 8;
|
||||
@@ -651,14 +660,15 @@ async function loadEquipmentMarkers(workplaceId) {
|
||||
|
||||
// 표시 이름: [코드] 이름
|
||||
const displayName = `[${eq.equipment_code}] ${eq.equipment_name}`;
|
||||
const movedBadge = eq.is_temporarily_moved ? ' 🚚' : '';
|
||||
|
||||
markersHtml += `
|
||||
<div class="equipment-marker ${statusClass}"
|
||||
style="left: ${eq.map_x_percent}%; top: ${eq.map_y_percent}%;
|
||||
width: ${width}%; height: ${height}%;"
|
||||
title="${displayName}"
|
||||
title="${displayName}${eq.is_temporarily_moved ? ' (임시이동)' : ''}"
|
||||
onclick="openEquipmentPanel(${JSON.stringify(eq).replace(/"/g, '"')})">
|
||||
<span class="marker-label">${displayName}</span>
|
||||
<span class="marker-label">${displayName}${movedBadge}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -747,6 +757,11 @@ function switchWorkplaceTab(tabName) {
|
||||
// 선택한 탭 활성화
|
||||
document.querySelector(`.workplace-tab[data-tab="${tabName}"]`).classList.add('active');
|
||||
document.getElementById(`tab-${tabName}`).classList.add('active');
|
||||
|
||||
// 이동 설비 탭 선택 시 데이터 로드
|
||||
if (tabName === 'moved-eq' && currentModalWorkplace) {
|
||||
loadWorkplaceMovedEquipments(currentModalWorkplace.workplace_id);
|
||||
}
|
||||
}
|
||||
|
||||
// 순회점검 페이지로 이동
|
||||
@@ -1527,6 +1542,159 @@ async function submitPanelReturn() {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 임시 이동 설비 목록 ====================
|
||||
|
||||
async function loadMovedEquipments() {
|
||||
const listEl = document.getElementById('movedEquipmentList');
|
||||
const emptyEl = document.getElementById('noMovedEquipment');
|
||||
|
||||
if (!listEl) return;
|
||||
|
||||
try {
|
||||
const response = await window.apiCall('/equipments/moved/list', 'GET');
|
||||
|
||||
if (response && response.success && response.data && response.data.length > 0) {
|
||||
const equipments = response.data;
|
||||
emptyEl.style.display = 'none';
|
||||
listEl.style.display = 'grid';
|
||||
|
||||
listEl.innerHTML = equipments.map(eq => `
|
||||
<div class="moved-equipment-card" onclick="showMovedEquipmentDetail(${eq.equipment_id})">
|
||||
<div class="moved-eq-header">
|
||||
<span class="moved-eq-code">${eq.equipment_code}</span>
|
||||
<span class="moved-eq-badge">임시이동</span>
|
||||
</div>
|
||||
<div class="moved-eq-name">${eq.equipment_name}</div>
|
||||
<div class="moved-eq-location">
|
||||
<div class="location-row">
|
||||
<span class="location-label">원래 위치</span>
|
||||
<span class="location-value">${eq.original_workplace_name || '-'}</span>
|
||||
</div>
|
||||
<div class="location-arrow">↓</div>
|
||||
<div class="location-row">
|
||||
<span class="location-label">현재 위치</span>
|
||||
<span class="location-value current">${eq.current_workplace_name || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moved-eq-date">
|
||||
이동일: ${formatPanelDate(eq.moved_at)}
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline" onclick="event.stopPropagation(); returnEquipmentToOriginal(${eq.equipment_id})">
|
||||
원위치 복귀
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
listEl.style.display = 'none';
|
||||
emptyEl.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('임시 이동 설비 로드 실패:', error);
|
||||
listEl.innerHTML = '<div style="text-align: center; padding: 20px; color: var(--gray-500);">로드 실패</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 작업장별 이동 설비 로드
|
||||
async function loadWorkplaceMovedEquipments(workplaceId) {
|
||||
const movedInList = document.getElementById('movedInEquipmentList');
|
||||
const movedOutList = document.getElementById('movedOutEquipmentList');
|
||||
const badge = document.getElementById('movedEqCountBadge');
|
||||
|
||||
try {
|
||||
const response = await window.apiCall('/equipments/moved/list', 'GET');
|
||||
|
||||
if (response && response.success && response.data) {
|
||||
const allMoved = response.data;
|
||||
|
||||
// 이 작업장으로 이동해 온 설비 (current_workplace_id = workplaceId)
|
||||
const movedIn = allMoved.filter(eq => eq.current_workplace_id === workplaceId);
|
||||
|
||||
// 이 작업장에서 다른 곳으로 이동한 설비 (workplace_id = workplaceId)
|
||||
const movedOut = allMoved.filter(eq => eq.workplace_id === workplaceId);
|
||||
|
||||
// 배지 업데이트
|
||||
const totalCount = movedIn.length + movedOut.length;
|
||||
if (totalCount > 0) {
|
||||
badge.textContent = totalCount;
|
||||
badge.style.display = 'inline-flex';
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
|
||||
// 이동해 온 설비 렌더링
|
||||
if (movedIn.length > 0) {
|
||||
movedInList.innerHTML = movedIn.map(eq => `
|
||||
<div class="moved-eq-item in">
|
||||
<div class="moved-eq-item-header">
|
||||
<span class="eq-code">${eq.equipment_code}</span>
|
||||
<span class="eq-name">${eq.equipment_name}</span>
|
||||
</div>
|
||||
<div class="moved-eq-item-info">
|
||||
<span class="from-location">📤 ${eq.original_workplace_name || '알 수 없음'}</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="to-location">📥 여기</span>
|
||||
</div>
|
||||
<div class="moved-eq-item-date">이동일: ${formatPanelDate(eq.moved_at)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
movedInList.innerHTML = '<p class="empty-message">없음</p>';
|
||||
}
|
||||
|
||||
// 이동해 간 설비 렌더링
|
||||
if (movedOut.length > 0) {
|
||||
movedOutList.innerHTML = movedOut.map(eq => `
|
||||
<div class="moved-eq-item out">
|
||||
<div class="moved-eq-item-header">
|
||||
<span class="eq-code">${eq.equipment_code}</span>
|
||||
<span class="eq-name">${eq.equipment_name}</span>
|
||||
</div>
|
||||
<div class="moved-eq-item-info">
|
||||
<span class="from-location">📤 여기</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="to-location">📥 ${eq.current_workplace_name || '알 수 없음'}</span>
|
||||
</div>
|
||||
<div class="moved-eq-item-date">이동일: ${formatPanelDate(eq.moved_at)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
movedOutList.innerHTML = '<p class="empty-message">없음</p>';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업장 이동 설비 로드 실패:', error);
|
||||
movedInList.innerHTML = '<p class="empty-message">로드 실패</p>';
|
||||
movedOutList.innerHTML = '<p class="empty-message">로드 실패</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// 원위치 복귀
|
||||
async function returnEquipmentToOriginal(equipmentId) {
|
||||
if (!confirm('이 설비를 원래 위치로 복귀시키겠습니까?')) return;
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(`/equipments/${equipmentId}/return`, 'POST');
|
||||
|
||||
if (response && response.success) {
|
||||
alert('설비가 원래 위치로 복귀되었습니다.');
|
||||
loadMovedEquipments();
|
||||
// 지도 새로고침
|
||||
if (selectedCategory) {
|
||||
renderMap();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('복귀 실패:', error);
|
||||
alert('복귀에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 이동된 설비 상세 보기
|
||||
function showMovedEquipmentDetail(equipmentId) {
|
||||
// TODO: 설비 상세 패널 열기
|
||||
console.log('설비 상세:', equipmentId);
|
||||
}
|
||||
|
||||
// ==================== 유틸리티 ====================
|
||||
|
||||
function formatPanelDate(dateStr) {
|
||||
@@ -1566,3 +1734,7 @@ window.submitPanelExport = submitPanelExport;
|
||||
window.openPanelReturnModal = openPanelReturnModal;
|
||||
window.closePanelReturnModal = closePanelReturnModal;
|
||||
window.submitPanelReturn = submitPanelReturn;
|
||||
window.loadMovedEquipments = loadMovedEquipments;
|
||||
window.returnEquipmentToOriginal = returnEquipmentToOriginal;
|
||||
window.showMovedEquipmentDetail = showMovedEquipmentDetail;
|
||||
window.loadWorkplaceMovedEquipments = loadWorkplaceMovedEquipments;
|
||||
|
||||
@@ -86,6 +86,26 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 임시 이동된 설비 현황 -->
|
||||
<section class="moved-equipment-section" style="margin-top: 24px;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="card-title">🚚 임시 이동된 설비</h2>
|
||||
<button class="btn btn-outline btn-sm" onclick="loadMovedEquipments()">새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="movedEquipmentList" class="moved-equipment-grid">
|
||||
<!-- 동적 로드 -->
|
||||
</div>
|
||||
<div id="noMovedEquipment" style="display: none; text-align: center; padding: 40px; color: var(--gray-500);">
|
||||
<div style="font-size: 48px; margin-bottom: 12px;">✅</div>
|
||||
<p>임시 이동된 설비가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
@@ -142,6 +162,11 @@
|
||||
<span class="tab-icon">🗺️</span>
|
||||
<span class="tab-text">상세 지도</span>
|
||||
</button>
|
||||
<button class="workplace-tab" data-tab="moved-eq" onclick="switchWorkplaceTab('moved-eq')">
|
||||
<span class="tab-icon">🚚</span>
|
||||
<span class="tab-text">이동 설비</span>
|
||||
<span id="movedEqCountBadge" class="tab-badge" style="display:none;">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 탭 콘텐츠 -->
|
||||
@@ -216,6 +241,33 @@
|
||||
</div>
|
||||
<div id="detailMapLegend" class="detail-map-legend"></div>
|
||||
</div>
|
||||
|
||||
<!-- 이동 설비 탭 -->
|
||||
<div id="tab-moved-eq" class="workplace-tab-content">
|
||||
<div class="moved-eq-tab-content">
|
||||
<!-- 이 작업장으로 이동해 온 설비 -->
|
||||
<div class="workplace-section">
|
||||
<h4 class="section-title">
|
||||
<span class="section-icon">📥</span>
|
||||
이 작업장으로 이동해 온 설비
|
||||
</h4>
|
||||
<div id="movedInEquipmentList" class="moved-eq-list">
|
||||
<p class="empty-message">없음</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 이 작업장에서 다른 곳으로 이동한 설비 -->
|
||||
<div class="workplace-section">
|
||||
<h4 class="section-title">
|
||||
<span class="section-icon">📤</span>
|
||||
다른 곳으로 이동한 설비
|
||||
</h4>
|
||||
<div id="movedOutEquipmentList" class="moved-eq-list">
|
||||
<p class="empty-message">없음</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user