- Headscale CLI 기반 제어 방식의 핵심 아키텍처 설명 - 이중 데이터베이스 전략 및 실시간 동기화 방법론 정리 - 실제 구현 코드 예시와 표준 패턴 제시 - Phase별 기능 확장 로드맵 및 개발 가이드라인 - 성능 최적화, 보안, 디버깅 방안 포함 - 향후 모든 기능 구현의 기준 문서로 활용 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
351 lines
12 KiB
Markdown
351 lines
12 KiB
Markdown
# FARMQ Admin 구현 가이드 및 설계 원칙
|
|
|
|
## 📋 개요
|
|
|
|
FARMQ Admin은 Headscale을 기반으로 한 100개 약국 네트워크 관리 시스템의 웹 인터페이스입니다. Headplane의 기능을 대체하면서 추가적인 약국 관리 기능을 제공하는 통합 관리 플랫폼입니다.
|
|
|
|
## 🏗️ 아키텍처 설계 원칙
|
|
|
|
### 핵심 설계 철학
|
|
```
|
|
FARMQ Admin (Frontend/API) → Headscale CLI (Backend Engine) → Network Management
|
|
```
|
|
|
|
**FARMQ Admin**은 **프론트엔드 인터페이스**이고, **Headscale**은 **백엔드 엔진**으로 작동합니다.
|
|
모든 네트워크 관리 기능은 **Headscale CLI를 통해 제어**되며, FARMQ Admin은 이를 웹 인터페이스로 래핑합니다.
|
|
|
|
### 계층 구조
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ FARMQ Admin │ ← 웹 UI, 약국 관리, 대시보드
|
|
│ (Flask + Bootstrap + JS) │
|
|
├─────────────────────────────────────┤
|
|
│ API Layer │ ← REST API, CLI 인터페이스
|
|
│ (Python subprocess calls) │
|
|
├─────────────────────────────────────┤
|
|
│ Headscale CLI │ ← 네트워크 관리 엔진
|
|
│ (Docker containerized) │
|
|
├─────────────────────────────────────┤
|
|
│ Database Layer │ ← 이중 데이터베이스
|
|
│ ┌─────────────┬─────────────────┐ │
|
|
│ │ FARMQ DB │ Headscale DB │ │
|
|
│ │ (약국정보) │ (노드정보) │ │
|
|
│ └─────────────┴─────────────────┘ │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
## 🔧 구현 방법론
|
|
|
|
### 1. CLI 기반 기능 구현 패턴
|
|
|
|
모든 Headscale 관련 기능은 다음 패턴을 따라 구현합니다:
|
|
|
|
```python
|
|
# 표준 구현 패턴
|
|
def headscale_function():
|
|
try:
|
|
# Docker를 통해 Headscale CLI 실행
|
|
result = subprocess.run(
|
|
['docker', 'exec', 'headscale', 'headscale', 'command', 'args'],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
|
|
# JSON 출력 파싱 (가능한 경우)
|
|
if '-o json' in args:
|
|
data = json.loads(result.stdout)
|
|
return data
|
|
|
|
return {'success': True, 'output': result.stdout}
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
return {'success': False, 'error': e.stderr}
|
|
```
|
|
|
|
#### 구현 예시들
|
|
|
|
**1. 실시간 온라인 상태 조회**
|
|
```python
|
|
def get_headscale_online_status() -> Dict[str, bool]:
|
|
"""Headscale CLI를 통해 실시간 온라인 상태 조회"""
|
|
result = subprocess.run([
|
|
'docker', 'exec', 'headscale',
|
|
'headscale', 'nodes', 'list', '-o', 'json'
|
|
], capture_output=True, text=True, check=True)
|
|
|
|
nodes_data = json.loads(result.stdout)
|
|
online_status = {}
|
|
|
|
for node in nodes_data:
|
|
node_name = node.get('given_name') or node.get('name', '')
|
|
is_online = node.get('online', False) == True
|
|
online_status[node_name.lower()] = is_online
|
|
|
|
return online_status
|
|
```
|
|
|
|
**2. 노드 삭제 기능**
|
|
```python
|
|
@app.route('/api/nodes/<int:node_id>/delete', methods=['DELETE'])
|
|
def api_delete_node(node_id):
|
|
"""노드 삭제 API"""
|
|
result = subprocess.run([
|
|
'docker', 'exec', 'headscale',
|
|
'headscale', 'nodes', 'delete',
|
|
'-i', str(node_id), '--force'
|
|
], capture_output=True, text=True, check=True)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'노드 {node_id}가 성공적으로 삭제되었습니다.'
|
|
})
|
|
```
|
|
|
|
### 2. 이중 데이터베이스 전략
|
|
|
|
#### FARMQ Database (자체 관리)
|
|
- **목적**: 약국 정보, 관리자 데이터, 커스텀 설정
|
|
- **특징**: 완전한 제어권, 외래키 제약 없음, 능동적 관리
|
|
- **테이블**: `pharmacy_info`, `machine_profiles`, `monitoring_metrics`, `system_alerts`
|
|
|
|
```python
|
|
# FARMQ DB - 약국 정보 관리
|
|
class PharmacyInfo(FarmqBase):
|
|
__tablename__ = 'pharmacy_info'
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
pharmacy_name = Column(String(100), nullable=False)
|
|
business_number = Column(String(20), unique=True)
|
|
manager_name = Column(String(50))
|
|
headscale_user_name = Column(String(50)) # Headscale과 연결점
|
|
```
|
|
|
|
#### Headscale Database (읽기 전용)
|
|
- **목적**: 네트워크 노드 정보, 실시간 상태
|
|
- **특징**: 읽기 전용 접근, Headscale이 관리
|
|
- **활용**: 실시간 쿼리로 최신 상태 반영
|
|
|
|
```python
|
|
# Headscale DB - 읽기 전용 조회
|
|
def get_dashboard_stats():
|
|
headscale_session = get_headscale_session()
|
|
|
|
# 실시간 노드 상태
|
|
active_nodes = headscale_session.query(Node).filter(
|
|
Node.deleted_at.is_(None)
|
|
).all()
|
|
|
|
# CLI로 온라인 상태 확인
|
|
online_status = get_headscale_online_status()
|
|
|
|
# 두 데이터 소스 결합
|
|
for node in active_nodes:
|
|
node_name = (node.given_name or '').lower()
|
|
is_online = online_status.get(node_name, False)
|
|
```
|
|
|
|
### 3. 실시간 동기화 전략
|
|
|
|
#### 기존 문제점
|
|
- 타임아웃 기반 온라인 판단 (부정확)
|
|
- 캐시된 데이터 사용 (지연)
|
|
- Headplane과 상태 불일치
|
|
|
|
#### 해결책: 직접 CLI 조회
|
|
```javascript
|
|
// 실시간 업데이트 (프론트엔드)
|
|
function updateStats() {
|
|
fetch('/api/dashboard/stats')
|
|
.then(response => response.json())
|
|
.then(stats => {
|
|
// Headplane과 동일한 3/5 온라인 표시
|
|
document.querySelector('[data-stat="online"]').textContent = stats.online_machines;
|
|
document.querySelector('[data-stat="offline"]').textContent = stats.offline_machines;
|
|
});
|
|
}
|
|
|
|
// 10초마다 업데이트 (Headplane보다 빠름)
|
|
setInterval(updateStats, 10000);
|
|
```
|
|
|
|
## 🚀 확장 가능한 기능 구현 로드맵
|
|
|
|
### Phase 1: 기본 Headplane 기능 대체 ✅
|
|
- [x] 실시간 노드 상태 동기화
|
|
- [x] 노드 삭제 기능
|
|
- [x] 대시보드 통계
|
|
- [x] 머신 목록 관리
|
|
|
|
### Phase 2: 고급 네트워크 관리 기능
|
|
- [ ] **노드 이름 변경**
|
|
```python
|
|
@app.route('/api/nodes/<int:node_id>/rename', methods=['POST'])
|
|
def api_rename_node(node_id):
|
|
new_name = request.json.get('new_name')
|
|
subprocess.run(['docker', 'exec', 'headscale',
|
|
'headscale', 'nodes', 'rename',
|
|
'-i', str(node_id), new_name])
|
|
```
|
|
|
|
- [ ] **노드 만료/로그아웃**
|
|
```python
|
|
@app.route('/api/nodes/<int:node_id>/expire', methods=['POST'])
|
|
def api_expire_node(node_id):
|
|
subprocess.run(['docker', 'exec', 'headscale',
|
|
'headscale', 'nodes', 'expire',
|
|
'-i', str(node_id)])
|
|
```
|
|
|
|
- [ ] **라우트 관리**
|
|
```python
|
|
@app.route('/api/nodes/<int:node_id>/routes', methods=['GET'])
|
|
def api_node_routes(node_id):
|
|
result = subprocess.run(['docker', 'exec', 'headscale',
|
|
'headscale', 'nodes', 'list-routes',
|
|
'-i', str(node_id), '-o', 'json'])
|
|
```
|
|
|
|
### Phase 3: 약국별 네트워크 관리
|
|
- [ ] **약국별 사용자 그룹 관리**
|
|
```python
|
|
@app.route('/api/pharmacy/<int:pharmacy_id>/users', methods=['GET'])
|
|
def api_pharmacy_users(pharmacy_id):
|
|
# 약국에 속한 Headscale 사용자 조회
|
|
pharmacy = get_pharmacy_by_id(pharmacy_id)
|
|
subprocess.run(['docker', 'exec', 'headscale',
|
|
'headscale', 'users', 'list', '-o', 'json'])
|
|
```
|
|
|
|
- [ ] **약국별 PreAuth Key 생성**
|
|
```python
|
|
@app.route('/api/pharmacy/<int:pharmacy_id>/preauth-key', methods=['POST'])
|
|
def api_create_pharmacy_preauth_key(pharmacy_id):
|
|
pharmacy = get_pharmacy_by_id(pharmacy_id)
|
|
user_name = pharmacy.headscale_user_name
|
|
|
|
subprocess.run(['docker', 'exec', 'headscale',
|
|
'headscale', 'preauthkeys', 'create',
|
|
'--user', user_name, '--reusable'])
|
|
```
|
|
|
|
### Phase 4: 고급 모니터링 및 자동화
|
|
- [ ] **실시간 네트워크 토폴로지**
|
|
- [ ] **자동 장애 감지 및 알림**
|
|
- [ ] **성능 메트릭 수집**
|
|
- [ ] **백업 및 복구 자동화**
|
|
|
|
## 🎯 개발 가이드라인
|
|
|
|
### 1. 모든 새 기능은 CLI 우선
|
|
```python
|
|
# ❌ 잘못된 접근
|
|
def bad_implementation():
|
|
# 직접 DB 조작 시도
|
|
session.execute("UPDATE nodes SET ...")
|
|
|
|
# ✅ 올바른 접근
|
|
def good_implementation():
|
|
# Headscale CLI 사용
|
|
subprocess.run(['docker', 'exec', 'headscale', 'headscale', 'command'])
|
|
```
|
|
|
|
### 2. 에러 처리 표준화
|
|
```python
|
|
def standard_error_handling():
|
|
try:
|
|
result = subprocess.run(headscale_command, check=True)
|
|
return {'success': True, 'data': result.stdout}
|
|
except subprocess.CalledProcessError as e:
|
|
return {'success': False, 'error': e.stderr}
|
|
except Exception as e:
|
|
return {'success': False, 'error': f'서버 오류: {str(e)}'}
|
|
```
|
|
|
|
### 3. UI 일관성 유지
|
|
```javascript
|
|
// 표준 삭제 확인 패턴
|
|
function confirmDelete(itemType, itemName, deleteFunction) {
|
|
if (confirm(`정말로 ${itemType} "${itemName}"를 삭제하시겠습니까?\n\n삭제된 항목은 복구할 수 없습니다.`)) {
|
|
deleteFunction();
|
|
}
|
|
}
|
|
|
|
// 표준 피드백 패턴
|
|
function showFeedback(message, type = 'info') {
|
|
showToast(message, type);
|
|
if (type === 'success') {
|
|
setTimeout(() => location.reload(), 1500);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🔍 디버깅 및 로깅
|
|
|
|
### CLI 호출 로깅
|
|
```python
|
|
def log_cli_call(command, result):
|
|
print(f"🔧 Headscale CLI: {' '.join(command)}")
|
|
print(f"📤 Output: {result.stdout}")
|
|
if result.stderr:
|
|
print(f"⚠️ Error: {result.stderr}")
|
|
```
|
|
|
|
### 프론트엔드 상태 디버깅
|
|
```javascript
|
|
// 개발 모드에서만 활성화
|
|
if (window.location.hostname === 'localhost') {
|
|
console.log('🔍 FARMQ Admin Debug Mode');
|
|
window.farmqDebug = {
|
|
showNodeStatus: () => console.table(onlineStatus),
|
|
refreshStats: updateStats,
|
|
testAPI: (endpoint) => fetch(endpoint).then(r => r.json())
|
|
};
|
|
}
|
|
```
|
|
|
|
## 📊 성능 최적화
|
|
|
|
### 1. CLI 호출 최적화
|
|
- JSON 출력 사용으로 파싱 효율화
|
|
- 불필요한 CLI 호출 최소화
|
|
- 결과 캐싱 (단기간)
|
|
|
|
### 2. 프론트엔드 최적화
|
|
- 실시간 업데이트 주기 조정 (10초)
|
|
- 필요한 데이터만 요청
|
|
- 사용자 상호작용 우선순위
|
|
|
|
## 🔐 보안 고려사항
|
|
|
|
### 1. CLI 명령 검증
|
|
```python
|
|
def validate_node_id(node_id):
|
|
if not isinstance(node_id, int) or node_id <= 0:
|
|
raise ValueError("Invalid node ID")
|
|
return node_id
|
|
|
|
def sanitize_command_args(args):
|
|
# 특수문자 및 인젝션 방지
|
|
return [arg for arg in args if is_safe_arg(arg)]
|
|
```
|
|
|
|
### 2. 권한 관리
|
|
- API 엔드포인트별 권한 확인
|
|
- 약국별 데이터 접근 제한
|
|
- 관리자/사용자 역할 구분
|
|
|
|
## 📝 결론
|
|
|
|
FARMQ Admin은 **Headscale CLI를 core engine으로 활용**하는 **웹 프론트엔드 래퍼**입니다.
|
|
이 접근 방식을 통해:
|
|
|
|
1. **Headplane과 100% 호환성** 유지
|
|
2. **실시간 정확한 상태** 반영
|
|
3. **확장 가능한 구조** 제공
|
|
4. **약국 특화 기능** 추가 가능
|
|
|
|
모든 새로운 기능은 이 원칙을 따라 구현하여 **일관성 있고 안정적인 시스템**을 구축합니다.
|
|
|
|
---
|
|
*Generated with [Claude Code](https://claude.ai/code) - FARMQ Admin Implementation Guide v1.0* |