Add pharmacy auto-registration and infrastructure improvements

- Auto-generate pharmacy_code (P001~P999) when creating new pharmacy
- Add new pharmacy fields: owner info, institution code/type, API port
- Change Headplane port mapping: 3000 → 3001 to avoid conflicts
- Add code-server setup script for development environment
- Add LXC Caddy setup documentation
- Update .gitignore to exclude farmq-admin submodule

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
PharmQ Admin
2025-11-02 07:54:47 +00:00
parent f739916737
commit 8d27461f76
6 changed files with 686 additions and 14 deletions

View File

@@ -42,31 +42,51 @@ class JSONType(TypeDecorator):
class PharmacyInfo(FarmqBase):
"""약국 정보 테이블 - Headscale과 독립적"""
__tablename__ = 'pharmacies'
id = Column(Integer, primary_key=True, autoincrement=True)
# 약국 코드 (핵심 식별자) - P001~P999
pharmacy_code = Column(String(10), unique=True)
# Headscale 연결 정보 (느슨한 결합)
headscale_user_name = Column(String(255)) # users.name 참조 (외래키 제약조건 없음)
headscale_user_id = Column(Integer) # users.id 참조 (외래키 제약조건 없음)
# 약국 기본 정보
pharmacy_name = Column(String(255), nullable=False)
business_number = Column(String(20))
manager_name = Column(String(100))
manager_name = Column(String(100)) # deprecated - use owner_name
phone = Column(String(20))
address = Column(Text)
# 기술적 정보
# 대표자 정보 (신규)
owner_name = Column(String(100))
owner_license = Column(String(50))
owner_phone = Column(String(20))
owner_email = Column(String(100))
# 요양기관 정보 (신규)
institution_code = Column(String(20))
institution_type = Column(String(20))
# 운영 정보 (신규)
opening_date = Column(DateTime)
business_hours = Column(Text)
# API 포트 (신규)
api_port = Column(Integer, default=8082)
# 기술적 정보 (deprecated - pharmacy_servers로 이동)
proxmox_host = Column(String(255))
proxmox_username = Column(String(100))
proxmox_api_token = Column(Text) # 암호화 권장
tailscale_ip = Column(String(45)) # IPv4/IPv6 지원
tailscale_ip = Column(String(45)) # IPv4/IPv6 지원 (deprecated)
# 상태 관리
status = Column(String(20), default='active') # active, inactive, maintenance
last_sync = Column(DateTime) # 마지막 동기화 시간
notes = Column(Text) # 관리 메모
# 타임스탬프
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
@@ -78,6 +98,7 @@ class PharmacyInfo(FarmqBase):
"""딕셔너리로 변환"""
return {
'id': self.id,
'pharmacy_code': self.pharmacy_code,
'headscale_user_name': self.headscale_user_name,
'headscale_user_id': self.headscale_user_id,
'pharmacy_name': self.pharmacy_name,
@@ -85,6 +106,15 @@ class PharmacyInfo(FarmqBase):
'manager_name': self.manager_name,
'phone': self.phone,
'address': self.address,
'owner_name': self.owner_name,
'owner_license': self.owner_license,
'owner_phone': self.owner_phone,
'owner_email': self.owner_email,
'institution_code': self.institution_code,
'institution_type': self.institution_type,
'opening_date': self.opening_date.isoformat() if self.opening_date else None,
'business_hours': self.business_hours,
'api_port': self.api_port,
'proxmox_host': self.proxmox_host,
'tailscale_ip': self.tailscale_ip,
'status': self.status,
@@ -179,6 +209,97 @@ class MachineProfile(FarmqBase):
}
class PharmacyServer(FarmqBase):
"""약국 서버 테이블 - 약국과 서버 분리"""
__tablename__ = 'pharmacy_servers'
id = Column(Integer, primary_key=True, autoincrement=True)
# 약국 연결
pharmacy_id = Column(Integer, nullable=False)
pharmacy_code = Column(String(10), nullable=False)
# Headscale 노드 연결
headscale_node_id = Column(Integer, unique=True)
headscale_user_id = Column(Integer)
# 네트워크 정보
vpn_ip = Column(String(45), nullable=False)
api_port = Column(Integer, default=8082)
is_online = Column(Boolean, default=False)
last_seen_at = Column(DateTime)
# 서버 역할
server_role = Column(String(20), default='primary') # primary, backup, test
is_active = Column(Boolean, default=True)
# 하드웨어 정보
hostname = Column(String(255))
machine_name = Column(String(255))
serial_number = Column(String(100))
cpu_model = Column(String(255))
cpu_cores = Column(Integer)
cpu_threads = Column(Integer)
ram_gb = Column(Integer)
storage_type = Column(String(50))
storage_gb = Column(Integer)
gpu_model = Column(String(255))
gpu_memory_gb = Column(Integer)
network_interfaces = Column(JSONType)
os_type = Column(String(50))
os_version = Column(String(100))
tailscale_version = Column(String(50))
installed_software = Column(JSONType)
# 관리 정보
status = Column(String(20), default='active')
location = Column(String(255))
purchase_date = Column(DateTime)
warranty_expires = Column(DateTime)
last_maintenance = Column(DateTime)
# 베이스라인 메트릭
baseline_cpu_temp = Column(Float)
baseline_cpu_usage = Column(Float)
baseline_memory_usage = Column(Float)
# 메타데이터
notes = Column(Text)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
def __repr__(self):
return f"<PharmacyServer(id={self.id}, pharmacy_code='{self.pharmacy_code}', vpn_ip='{self.vpn_ip}')>"
def to_dict(self) -> Dict[str, Any]:
return {
'id': self.id,
'pharmacy_id': self.pharmacy_id,
'pharmacy_code': self.pharmacy_code,
'headscale_node_id': self.headscale_node_id,
'vpn_ip': self.vpn_ip,
'api_port': self.api_port,
'is_online': self.is_online,
'last_seen_at': self.last_seen_at.isoformat() if self.last_seen_at else None,
'server_role': self.server_role,
'is_active': self.is_active,
'hostname': self.hostname,
'machine_name': self.machine_name,
'cpu_model': self.cpu_model,
'cpu_cores': self.cpu_cores,
'ram_gb': self.ram_gb,
'storage_gb': self.storage_gb,
'os_type': self.os_type,
'os_version': self.os_version,
'status': self.status,
'location': self.location,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat()
}
class MonitoringMetrics(FarmqBase):
"""실시간 모니터링 메트릭스 - 시계열 데이터"""
__tablename__ = 'monitoring_metrics'