feat: 임시 이동 설비 현황 표시 기능 추가

- 대시보드 하단에 임시 이동된 설비 카드 섹션 추가
- 작업장 모달에 '이동 설비' 탭 추가
  - 이 작업장으로 이동해 온 설비 표시
  - 다른 곳으로 이동한 설비 표시
- 설비 마커에 이동 상태 색상 구분 (주황색 점선 + 깜빡임)
- 원위치 복귀 기능
- 사이드바 기본값을 접힌 상태로 변경

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-04 14:30:25 +09:00
parent 4d83f10b07
commit d1aec517a6
5 changed files with 415 additions and 9 deletions

View File

@@ -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;
}

View File

@@ -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');

View File

@@ -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) {

View File

@@ -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, '&quot;')})">
<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;

View File

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