팜큐 관리자 시스템 구현 가이드 및 설계 원칙 문서 추가
- Headscale CLI 기반 제어 방식의 핵심 아키텍처 설명 - 이중 데이터베이스 전략 및 실시간 동기화 방법론 정리 - 실제 구현 코드 예시와 표준 패턴 제시 - Phase별 기능 확장 로드맵 및 개발 가이드라인 - 성능 최적화, 보안, 디버깅 방안 포함 - 향후 모든 기능 구현의 기준 문서로 활용 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
45c952258b
commit
24cf84fda3
351
FARMQ_ADMIN_IMPLEMENTATION_GUIDE.md
Normal file
351
FARMQ_ADMIN_IMPLEMENTATION_GUIDE.md
Normal file
@ -0,0 +1,351 @@
|
||||
# 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*
|
||||
Loading…
Reference in New Issue
Block a user