🏥 Add complete FARMQ Admin Flask application

## Features
- 한국어 Flask 관리 인터페이스 with Bootstrap 5
- Headscale과 분리된 독립 데이터베이스 구조
- 약국 관리 시스템 (pharmacy management)
- 머신 모니터링 및 상태 관리
- 실시간 대시보드 with 통계 및 알림
- Headscale 사용자명과 약국명 분리 관리

## Database Architecture
- 별도 FARMQ SQLite DB (farmq.sqlite)
- Headscale DB와 외래키 충돌 방지
- 느슨한 결합 설계 (ID 참조만 사용)

## UI Components
- 반응형 대시보드 with 실시간 통계
- 약국별 머신 상태 모니터링
- 한국어 지역화 및 사용자 친화적 인터페이스
- 머신 온라인/오프라인 상태 표시 (24시간 타임아웃)

## API Endpoints
- `/api/sync/machines` - Headscale 머신 동기화
- `/api/sync/users` - Headscale 사용자 동기화
- `/api/pharmacy/<id>/update` - 약국 정보 업데이트
- 대시보드 통계 및 알림 API

## Problem Resolution
- Fixed foreign key conflicts preventing Windows client connections
- Resolved machine online status detection with proper timeout handling
- Separated technical Headscale usernames from business pharmacy names

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-09 17:44:56 +09:00
parent 9155bf5479
commit ca61a89739
16 changed files with 3824 additions and 0 deletions

View File

@@ -0,0 +1,388 @@
{% extends "base.html" %}
{% block title %}머신 상세 정보 - 팜큐 약국 관리 시스템{% endblock %}
{% block breadcrumb %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('dashboard') }}">대시보드</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('machine_list') }}">머신 관리</a></li>
<li class="breadcrumb-item active">{{ machine.given_name or machine.hostname }}</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<!-- 머신 정보 헤더 -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<div class="me-3">
{% if is_online %}
<i class="fas fa-desktop fa-3x text-success"></i>
{% else %}
<i class="fas fa-desktop fa-3x text-muted"></i>
{% endif %}
</div>
<div>
<h1 class="h2 mb-0">{{ machine.given_name or machine.hostname }}</h1>
<p class="text-muted mb-1">{{ machine.hostname }}</p>
<div class="d-flex gap-2 align-items-center">
{% if is_online %}
<span class="badge bg-success">
<i class="fas fa-circle"></i> 온라인
</span>
{% else %}
<span class="badge bg-danger">
<i class="fas fa-circle"></i> 오프라인
</span>
{% endif %}
<small class="text-muted">마지막 접속: {{ last_seen_humanized }}</small>
</div>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary" onclick="refreshMachineDetail()">
<i class="fas fa-sync-alt"></i> 새로고침
</button>
{% if is_online %}
<button class="btn btn-outline-warning">
<i class="fas fa-redo"></i> 재시작
</button>
{% endif %}
<button class="btn btn-outline-info" onclick="showMonitoringModal()">
<i class="fas fa-chart-line"></i> 실시간 모니터링
</button>
</div>
</div>
</div>
</div>
<!-- 기본 정보 및 네트워크 -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> 기본 정보</h5>
</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<th width="30%">머신 ID</th>
<td>{{ machine.id }}</td>
</tr>
<tr>
<th>호스트명</th>
<td>{{ machine.hostname }}</td>
</tr>
<tr>
<th>표시 이름</th>
<td>{{ machine.given_name or '미설정' }}</td>
</tr>
<tr>
<th>사용자</th>
<td>
{% if machine.user %}
<span class="badge bg-primary">{{ machine.user.name }}</span>
{% else %}
<span class="text-muted">미지정</span>
{% endif %}
</td>
</tr>
<tr>
<th>등록 방식</th>
<td>{{ machine.register_method or '알 수 없음' }}</td>
</tr>
<tr>
<th>등록일</th>
<td>{{ machine.created_at.strftime('%Y년 %m월 %d일 %H:%M') if machine.created_at else '알 수 없음' }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-network-wired"></i> 네트워크 정보</h5>
</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<th width="30%">IPv4 주소</th>
<td><code>{{ machine.ipv4 }}</code></td>
</tr>
{% if machine.ipv6 %}
<tr>
<th>IPv6 주소</th>
<td><code class="small">{{ machine.ipv6 }}</code></td>
</tr>
{% endif %}
<tr>
<th>엔드포인트</th>
<td>
{% if machine.get_endpoints() %}
<div class="small">
{% for endpoint in machine.get_endpoints()[:3] %}
<div><code>{{ endpoint }}</code></div>
{% endfor %}
{% if machine.get_endpoints()|length > 3 %}
<div class="text-muted">... 및 {{ machine.get_endpoints()|length - 3 }}개 더</div>
{% endif %}
</div>
{% else %}
<span class="text-muted">없음</span>
{% endif %}
</td>
</tr>
<tr>
<th>마지막 접속</th>
<td>
{% if machine.last_seen %}
{{ machine.last_seen.strftime('%Y-%m-%d %H:%M:%S') }}
<br><small class="text-muted">{{ last_seen_humanized }}</small>
{% else %}
<span class="text-muted">알 수 없음</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<!-- 하드웨어 사양 -->
{% if specs %}
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-microchip"></i> 하드웨어 사양</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-microchip fa-2x text-primary mb-2"></i>
<h6>CPU</h6>
<p class="mb-1">{{ specs.cpu_model }}</p>
<small class="text-muted">{{ specs.cpu_cores }}코어</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-memory fa-2x text-success mb-2"></i>
<h6>메모리</h6>
<p class="mb-1">{{ specs.ram_gb }}GB</p>
<small class="text-muted">RAM</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-hdd fa-2x text-warning mb-2"></i>
<h6>저장소</h6>
<p class="mb-1">{{ specs.storage_gb }}GB</p>
<small class="text-muted">디스크</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<i class="fas fa-wifi fa-2x text-info mb-2"></i>
<h6>네트워크</h6>
<p class="mb-1">{{ specs.network_speed }}Mbps</p>
<small class="text-muted">{{ specs.os_info or '알 수 없음' }}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- 현재 상태 모니터링 -->
{% if latest_monitoring %}
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-chart-line"></i> 현재 상태</h5>
<small class="text-muted">최종 업데이트: {{ latest_monitoring.collected_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="text-center">
<canvas id="cpuChart" width="100" height="100"></canvas>
<h6 class="mt-2">CPU 사용률</h6>
<span class="h4">{{ "%.1f"|format(latest_monitoring.cpu_usage|float) }}%</span>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<canvas id="memoryChart" width="100" height="100"></canvas>
<h6 class="mt-2">메모리 사용률</h6>
<span class="h4">{{ "%.1f"|format(latest_monitoring.memory_usage|float) }}%</span>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<canvas id="diskChart" width="100" height="100"></canvas>
<h6 class="mt-2">디스크 사용률</h6>
<span class="h4">{{ "%.1f"|format(latest_monitoring.disk_usage|float) }}%</span>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<div class="mb-2">
<i class="fas fa-thermometer-half fa-3x
{% if latest_monitoring.cpu_temperature > 80 %}text-danger
{% elif latest_monitoring.cpu_temperature > 70 %}text-warning
{% else %}text-success{% endif %}"></i>
</div>
<h6>CPU 온도</h6>
<span class="h4
{% if latest_monitoring.cpu_temperature > 80 %}text-danger
{% elif latest_monitoring.cpu_temperature > 70 %}text-warning
{% else %}text-success{% endif %}">
{{ latest_monitoring.cpu_temperature }}°C
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- 소속 약국 정보 -->
{% if pharmacy %}
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-store"></i> 소속 약국</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>{{ pharmacy.pharmacy_name }}</h6>
<p class="text-muted">{{ pharmacy.address or '주소 미등록' }}</p>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between">
<div>
<strong>담당자:</strong> {{ pharmacy.manager_name or '미등록' }}
</div>
<div>
<strong>연락처:</strong> {{ pharmacy.phone or '미등록' }}
</div>
</div>
<div class="mt-2">
<a href="{{ url_for('pharmacy_detail', pharmacy_id=pharmacy.id) }}"
class="btn btn-outline-primary btn-sm">
<i class="fas fa-eye"></i> 약국 상세 보기
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- 실시간 모니터링 모달 -->
<div class="modal fade" id="monitoringModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-chart-line"></i> 실시간 모니터링 - {{ machine.hostname }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<canvas id="realtimeCpuChart"></canvas>
</div>
<div class="col-md-6">
<canvas id="realtimeMemoryChart"></canvas>
</div>
</div>
<div class="mt-3 text-center">
<div id="monitoringStatus" class="alert alert-info">
실시간 데이터를 불러오는 중...
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let monitoringModal;
document.addEventListener('DOMContentLoaded', function() {
monitoringModal = new bootstrap.Modal(document.getElementById('monitoringModal'));
{% if latest_monitoring %}
// 도넛 차트 생성
createDoughnutChart('cpuChart', {{ latest_monitoring.cpu_usage|float }}, 'CPU', '#007bff');
createDoughnutChart('memoryChart', {{ latest_monitoring.memory_usage|float }}, 'Memory', '#28a745');
createDoughnutChart('diskChart', {{ latest_monitoring.disk_usage|float }}, 'Disk', '#ffc107');
{% endif %}
});
function refreshMachineDetail() {
showToast('머신 정보를 새로고침 중...', 'info');
setTimeout(() => {
location.reload();
}, 1000);
}
function showMonitoringModal() {
monitoringModal.show();
loadRealtimeData();
}
function loadRealtimeData() {
fetch(`/api/machines/{{ machine.id }}/monitoring`)
.then(response => response.json())
.then(data => {
document.getElementById('monitoringStatus').innerHTML =
`<i class="fas fa-check-circle"></i> 최근 ${data.length}개 데이터 포인트 로드됨`;
document.getElementById('monitoringStatus').className = 'alert alert-success';
// 실시간 차트 업데이트 (구현 예정)
console.log('Monitoring data:', data);
})
.catch(error => {
document.getElementById('monitoringStatus').innerHTML =
`<i class="fas fa-exclamation-triangle"></i> 데이터 로드 실패: ${error.message}`;
document.getElementById('monitoringStatus').className = 'alert alert-danger';
});
}
// 10초마다 현재 상태 업데이트
setInterval(() => {
if ({{ machine.id }}) {
updateCurrentStatus({{ machine.id }});
}
}, 10000);
function updateCurrentStatus(machineId) {
// 실시간 상태 업데이트 구현 (향후)
}
</script>
{% endblock %}

View File

@@ -0,0 +1,397 @@
{% extends "base.html" %}
{% block title %}머신 관리 - 팜큐 약국 관리 시스템{% endblock %}
{% block breadcrumb %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('dashboard') }}">대시보드</a></li>
<li class="breadcrumb-item active">머신 관리</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h2 mb-0">
<i class="fas fa-desktop text-primary"></i>
머신 관리
</h1>
<p class="text-muted">연결된 모든 머신의 상태 및 하드웨어 정보</p>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary" onclick="refreshMachineList()">
<i class="fas fa-sync-alt"></i> 새로고침
</button>
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="viewMode" id="listView" autocomplete="off" checked>
<label class="btn btn-outline-primary" for="listView">
<i class="fas fa-list"></i> 목록
</label>
<input type="radio" class="btn-check" name="viewMode" id="cardView" autocomplete="off">
<label class="btn btn-outline-primary" for="cardView">
<i class="fas fa-th-large"></i> 카드
</label>
</div>
</div>
</div>
</div>
</div>
<!-- 필터 및 검색 -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-3 mb-2">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" id="searchMachine" placeholder="머신 검색...">
</div>
</div>
<div class="col-md-2 mb-2">
<select class="form-select" id="filterStatus">
<option value="">전체 상태</option>
<option value="online">온라인</option>
<option value="offline">오프라인</option>
</select>
</div>
<div class="col-md-2 mb-2">
<select class="form-select" id="filterPharmacy">
<option value="">전체 약국</option>
<!-- 약국 목록은 동적으로 로드 -->
</select>
</div>
<div class="col-md-3 mb-2">
<div class="d-flex gap-2">
<span class="badge bg-success">온라인: <span id="onlineCount">0</span></span>
<span class="badge bg-danger">오프라인: <span id="offlineCount">0</span></span>
<span class="badge bg-secondary">전체: <span id="totalCount">0</span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 머신 목록 (테이블 뷰) -->
<div id="listView" class="machine-view">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
{% if machines %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>머신 정보</th>
<th>네트워크</th>
<th>하드웨어</th>
<th>상태</th>
<th>소속 약국</th>
<th>액션</th>
</tr>
</thead>
<tbody>
{% for machine_data in machines %}
<tr class="machine-row" data-status="{% if machine_data.is_online %}online{% else %}offline{% endif %}">
<td>
<div class="d-flex align-items-center">
<div class="me-3">
{% if machine_data.is_online %}
<i class="fas fa-desktop fa-2x text-success"></i>
{% else %}
<i class="fas fa-desktop fa-2x text-muted"></i>
{% endif %}
</div>
<div>
<strong>{{ machine_data.machine_name or machine_data.hostname }}</strong>
<div class="small text-muted">{{ machine_data.hostname }}</div>
<div class="small">
<i class="fas fa-user"></i> {{ machine_data.headscale_user_name or '미지정' }}
</div>
</div>
</div>
</td>
<td>
<div>
<code class="small">{{ machine_data.tailscale_ip }}</code>
</div>
{% if machine_data.ipv6 %}
<div>
<code class="small text-muted">{{ machine_data.ipv6 }}</code>
</div>
{% endif %}
<div class="small text-muted">
엔드포인트: 0개
</div>
</td>
<td>
{% if machine_data.specs %}
<div class="small">
<div><i class="fas fa-microchip"></i> {{ machine_data.specs.cpu_model[:20] }}{% if machine_data.specs.cpu_model|length > 20 %}...{% endif %}</div>
<div><i class="fas fa-memory"></i> {{ machine_data.specs.ram_gb }}GB RAM</div>
<div><i class="fas fa-hdd"></i> {{ machine_data.specs.storage_gb }}GB</div>
</div>
{% else %}
<span class="text-muted small">정보 없음</span>
{% endif %}
</td>
<td>
<div class="d-flex flex-column">
{% if machine_data.is_online %}
<span class="badge bg-success mb-1">
<i class="fas fa-circle"></i> 온라인
</span>
{% else %}
<span class="badge bg-danger mb-1">
<i class="fas fa-circle"></i> 오프라인
</span>
{% endif %}
{% if machine_data.latest_monitoring %}
<div class="small">
<div>CPU: {{ machine_data.latest_monitoring.cpu_usage }}%</div>
<div>온도: {{ machine_data.latest_monitoring.cpu_temperature }}°C</div>
</div>
{% endif %}
</div>
</td>
<td>
{% if machine_data.pharmacy %}
<div>
<strong>{{ machine_data.pharmacy.pharmacy_name }}</strong>
<div class="small text-muted">{{ machine_data.pharmacy.manager_name }}</div>
</div>
{% else %}
<span class="text-muted">미지정</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('machine_detail', machine_id=machine_data.id) }}"
class="btn btn-outline-primary" title="상세 정보">
<i class="fas fa-eye"></i>
</a>
<button class="btn btn-outline-info"
onclick="showMonitoring({{ machine_data.id }})" title="모니터링">
<i class="fas fa-chart-line"></i>
</button>
{% if machine_data.is_online %}
<button class="btn btn-outline-warning" title="재시작">
<i class="fas fa-redo"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center text-muted py-5">
<i class="fas fa-desktop fa-4x mb-4 text-secondary"></i>
<h4>연결된 머신이 없습니다</h4>
<p class="mb-4">아직 등록된 머신이 없습니다. Headscale에 머신을 연결해주세요.</p>
<a href="http://localhost:3000/admin/" target="_blank" class="btn btn-primary">
<i class="fas fa-external-link-alt"></i> Headplane에서 머신 등록
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- 머신 목록 (카드 뷰) -->
<div id="cardView" class="machine-view d-none">
<div class="row">
{% for machine_data in machines %}
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100 machine-card" data-status="{% if machine_data.is_online %}online{% else %}offline{% endif %}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h5 class="card-title mb-1">{{ machine_data.machine_name or machine_data.hostname }}</h5>
<p class="card-text text-muted small">{{ machine_data.hostname }}</p>
</div>
{% if machine_data.is_online %}
<span class="badge bg-success">온라인</span>
{% else %}
<span class="badge bg-danger">오프라인</span>
{% endif %}
</div>
<div class="mb-3">
<div class="small mb-2">
<i class="fas fa-network-wired"></i> {{ machine_data.tailscale_ip }}
</div>
{% if machine_data.pharmacy %}
<div class="small mb-2">
<i class="fas fa-store"></i> {{ machine_data.pharmacy.pharmacy_name }}
</div>
{% endif %}
<div class="small">
<i class="fas fa-clock"></i> {{ machine_data.last_seen_humanized }}
</div>
</div>
{% if machine_data.specs %}
<div class="mb-3">
<hr>
<div class="row text-center">
<div class="col-4">
<div class="small text-muted">CPU</div>
<div class="small">{{ machine_data.specs.cpu_cores }}코어</div>
</div>
<div class="col-4">
<div class="small text-muted">RAM</div>
<div class="small">{{ machine_data.specs.ram_gb }}GB</div>
</div>
<div class="col-4">
<div class="small text-muted">Storage</div>
<div class="small">{{ machine_data.specs.storage_gb }}GB</div>
</div>
</div>
</div>
{% endif %}
{% if machine_data.latest_monitoring %}
<div class="mb-3">
<div class="row">
<div class="col-6">
<div class="small text-muted">CPU 사용률</div>
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-primary"
style="width: {{ machine_data.latest_monitoring.cpu_usage }}%"></div>
</div>
<div class="small">{{ machine_data.latest_monitoring.cpu_usage }}%</div>
</div>
<div class="col-6">
<div class="small text-muted">온도</div>
<div class="text-center">
<span class="h6 {% if machine_data.latest_monitoring.cpu_temperature > 80 %}text-danger{% elif machine_data.latest_monitoring.cpu_temperature > 70 %}text-warning{% else %}text-success{% endif %}">
{{ machine_data.latest_monitoring.cpu_temperature }}°C
</span>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<div class="card-footer bg-transparent">
<div class="d-grid gap-2 d-md-block">
<a href="{{ url_for('machine_detail', machine_id=machine_data.id) }}"
class="btn btn-outline-primary btn-sm">
<i class="fas fa-eye"></i> 상세
</a>
<button class="btn btn-outline-info btn-sm"
onclick="showMonitoring({{ machine_data.id }})">
<i class="fas fa-chart-line"></i> 모니터링
</button>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 뷰 모드 전환
document.querySelectorAll('input[name="viewMode"]').forEach(radio => {
radio.addEventListener('change', function() {
document.querySelectorAll('.machine-view').forEach(view => {
view.classList.add('d-none');
});
if (this.id === 'listView') {
document.getElementById('listView').classList.remove('d-none');
} else {
document.getElementById('cardView').classList.remove('d-none');
}
});
});
// 머신 검색
document.getElementById('searchMachine').addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
filterMachines();
});
// 상태 필터
document.getElementById('filterStatus').addEventListener('change', function() {
filterMachines();
});
function filterMachines() {
const searchTerm = document.getElementById('searchMachine').value.toLowerCase();
const statusFilter = document.getElementById('filterStatus').value;
let visibleCount = 0;
let onlineCount = 0;
let offlineCount = 0;
document.querySelectorAll('.machine-row, .machine-card').forEach(element => {
const machineText = element.textContent.toLowerCase();
const machineStatus = element.dataset.status;
let showElement = true;
// 검색어 필터
if (searchTerm && !machineText.includes(searchTerm)) {
showElement = false;
}
// 상태 필터
if (statusFilter && machineStatus !== statusFilter) {
showElement = false;
}
if (showElement) {
element.style.display = '';
visibleCount++;
if (machineStatus === 'online') onlineCount++;
else offlineCount++;
} else {
element.style.display = 'none';
}
});
// 카운터 업데이트
document.getElementById('onlineCount').textContent = onlineCount;
document.getElementById('offlineCount').textContent = offlineCount;
document.getElementById('totalCount').textContent = visibleCount;
}
// 모니터링 모달
function showMonitoring(machineId) {
// TODO: 모니터링 모달 구현
showToast(`머신 ${machineId} 모니터링 기능 준비 중`, 'info');
}
// 머신 목록 새로고침
function refreshMachineList() {
showToast('머신 목록을 새로고침 중...', 'info');
setTimeout(() => {
location.reload();
}, 1000);
}
// 초기 카운터 설정
document.addEventListener('DOMContentLoaded', function() {
filterMachines();
});
</script>
{% endblock %}