headscale-tailscale-replace.../FARMQ_ADMIN_IMPLEMENTATION_GUIDE.md
시골약사 24cf84fda3 팜큐 관리자 시스템 구현 가이드 및 설계 원칙 문서 추가
- Headscale CLI 기반 제어 방식의 핵심 아키텍처 설명
- 이중 데이터베이스 전략 및 실시간 동기화 방법론 정리
- 실제 구현 코드 예시와 표준 패턴 제시
- Phase별 기능 확장 로드맵 및 개발 가이드라인
- 성능 최적화, 보안, 디버깅 방안 포함
- 향후 모든 기능 구현의 기준 문서로 활용

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 10:49:48 +09:00

12 KiB

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 관련 기능은 다음 패턴을 따라 구현합니다:

# 표준 구현 패턴
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. 실시간 온라인 상태 조회

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. 노드 삭제 기능

@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
# 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이 관리
  • 활용: 실시간 쿼리로 최신 상태 반영
# 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 조회

// 실시간 업데이트 (프론트엔드)
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 기능 대체

  • 실시간 노드 상태 동기화
  • 노드 삭제 기능
  • 대시보드 통계
  • 머신 목록 관리

Phase 2: 고급 네트워크 관리 기능

  • 노드 이름 변경

    @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])
    
  • 노드 만료/로그아웃

    @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)])
    
  • 라우트 관리

    @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: 약국별 네트워크 관리

  • 약국별 사용자 그룹 관리

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

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

# ❌ 잘못된 접근
def bad_implementation():
    # 직접 DB 조작 시도
    session.execute("UPDATE nodes SET ...")

# ✅ 올바른 접근  
def good_implementation():
    # Headscale CLI 사용
    subprocess.run(['docker', 'exec', 'headscale', 'headscale', 'command'])

2. 에러 처리 표준화

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 일관성 유지

// 표준 삭제 확인 패턴
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 호출 로깅

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}")

프론트엔드 상태 디버깅

// 개발 모드에서만 활성화
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 명령 검증

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 - FARMQ Admin Implementation Guide v1.0