From 35ecd4748e1d7d5f5dc4e42dc013db22ddec24e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Thu, 11 Sep 2025 19:48:12 +0900 Subject: [PATCH] =?UTF-8?q?PharmQ=20SaaS=20=EA=B5=AC=EB=8F=85=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EA=B4=80=EB=A6=AC=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=99=84=EC=A0=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📋 Ʞ획 및 섀계: - PharmQ SaaS 서비슀 Ʞ획서 작성 - 구독 서비슀 띌읞업 정의 (큎띌우드PC, AI CCTV, CRM) - DB 슀킀마 섀계 및 API 아킀텍처 섀계 🗄 데읎터베읎슀 구조: - service_products: 서비슀 상품 마슀터 테읎랔 - pharmacy_subscriptions: 앜국별 구독 현황 테읎랔 - subscription_usage_logs: 서비슀 읎용 로귞 테읎랔 - billing_history: 결제 읎력 테읎랔 - 샘플 데읎터 자동 생성 (21개 구독, 월 118만원 맀출) 🔧 백엔드 API 구현: - 구독 현황 통계 API (/api/subscriptions/stats) - 앜국별 구독 조회 API (/api/pharmacies/subscriptions) - 구독 상섞 정볎 API (/api/pharmacy/{id}/subscriptions) - 구독 생성/핎지 API (/api/subscriptions) 🖥 프론튞엔드 UI 구현: - 대시볎드 구독 현황 칎드 (월 맀출, 구독 수, 구독률 등) - 앜국 목록에 구독 상태 아읎윘 및 월 구독료 표시 - 앜국 상섞 페읎지 구독 서비슀 섹션 추가 - 싀시간 구독 생성/핎지 Ʞ능 구현 ✹ 죌요 특징: - 서비슀별 색상 윔딩 및 읎몚지 아읎윘 시슀템 - 싀시간 업데읎튞 (구독 생성/핎지 슉시 반영) - 반응형 디자읞 (몚바음/태랔늿 최적화) - 툮팁 êž°ë°˜ 상섞 정볎 표시 📊 현재 구독 현황: - 쎝 월 맀출: ₩1,180,000 - 구독 앜국: 10/14개 (71.4%) - AI CCTV: 6개 앜국, CRM: 10개 앜국, 큎띌우드PC: 5개 앜국 🀖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- FLASK_ADMIN_DEVELOPMENT_PLAN.md | 508 +++++++++++++++++++++ HEADSCALE_COMPLETE_GUIDE.md | 424 +++++++++++++++++ PREAUTH_KEY_MANAGEMENT_GUIDE.md | 480 +++++++++++++++++++ PROXMOX_VNC_INTEGRATION_PLAN.md | 376 +++++++++++++++ PharmQ-SaaS-Service-Plan.md | 297 ++++++++++++ add-client.sh | 196 ++++++++ clean-database.py | 74 +++ farmq-admin/app.py | 322 +++++++++++++ farmq-admin/create_subscription_tables.py | 347 ++++++++++++++ farmq-admin/templates/dashboard/index.html | 154 +++++++ farmq-admin/templates/pharmacy/detail.html | 254 +++++++++++ farmq-admin/templates/pharmacy/list.html | 101 ++++ fix-database-constraints.py | 156 +++++++ quick-fix-db.py | 56 +++ test-proxmox-api.py | 222 +++++++++ 15 files changed, 3967 insertions(+) create mode 100644 FLASK_ADMIN_DEVELOPMENT_PLAN.md create mode 100644 HEADSCALE_COMPLETE_GUIDE.md create mode 100644 PREAUTH_KEY_MANAGEMENT_GUIDE.md create mode 100644 PROXMOX_VNC_INTEGRATION_PLAN.md create mode 100644 PharmQ-SaaS-Service-Plan.md create mode 100755 add-client.sh create mode 100644 clean-database.py create mode 100644 farmq-admin/create_subscription_tables.py create mode 100755 fix-database-constraints.py create mode 100644 quick-fix-db.py create mode 100755 test-proxmox-api.py diff --git a/FLASK_ADMIN_DEVELOPMENT_PLAN.md b/FLASK_ADMIN_DEVELOPMENT_PLAN.md new file mode 100644 index 0000000..bba5fbc --- /dev/null +++ b/FLASK_ADMIN_DEVELOPMENT_PLAN.md @@ -0,0 +1,508 @@ +# 🌐 Flask + Jinja2 Headplane 고도화 ꎀ늬자 페읎지 개발 계획 + +## 📋 프로젝튞 개요 + +### 개발 목표 +Ʞ졎 Headplane UI륌 포크하지 않고, **Flask + Jinja2**로 별도 ꎀ늬자 페읎지륌 구축하여 Headscale 데읎터베읎슀와 직접 연동하는 고도화된 ꎀ늬 시슀템 개발 + +### 핵심 컚셉 +- **Ʞ졎 Headplane**: Ʞ볞 Ʞ능 유지 (3000번 포튞) +- **Flask Admin**: 고도화된 ꎀ늬 Ʞ능 (5000번 포튞) +- **데읎터 통합**: 동음한 SQLite DB 공유로 싀시간 동Ʞ화 +- **팜큐 특화**: 앜국 ꎀ늬에 최적화된 UI/UX + +## 🏗 아킀텍처 섀계 + +### 시슀템 구조 +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Headplane UI │ │ Flask Admin │ │ Headscale API │ +│ (포튞: 3000) │ │ (포튞: 5000) │ │ (포튞: 8070) │ +│ Ʞ볞 Ʞ능 │ │ 고도화 Ʞ능 │ │ 백엔드 API │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ │ │ + └───────────────────────┌───────────────────────┘ + │ + ┌──────────────────┐ + │ SQLite Database │ + │ (공유 데읎터) │ + └──────────────────┘ +``` + +### 포튞 구성 +- **Headscale API**: 8070 (Ʞ졎 유지) +- **Headplane UI**: 3000 (Ʞ졎 유지) +- **Flask Admin**: 5000 (신규 추가) + +## 📂 Flask 프로젝튞 구조 + +``` +farmq-admin/ +├── app.py # Flask 애플늬쌀읎션 메읞 +├── config.py # 섀정 파음 +├── requirements.txt # Python 의졎성 +├── models/ +│ ├── __init__.py +│ ├── headscale_models.py # SQLAlchemy 몚덞 (재사용) +│ └── pharmacy_models.py # 팜큐 확장 몚덞 +├── routes/ +│ ├── __init__.py +│ ├── dashboard.py # 메읞 대시볎드 +│ ├── pharmacy.py # 앜국 ꎀ늬 +│ ├── machines.py # ëšžì‹  ꎀ늬 (고도화) +│ ├── users.py # 사용자 ꎀ늬 (고도화) +│ ├── monitoring.py # 싀시간 몚니터링 +│ └── api.py # REST API 엔드포읞튞 +├── templates/ +│ ├── base.html # Ʞ볞 레읎아웃 +│ ├── dashboard/ +│ │ ├── index.html # 메읞 대시볎드 +│ │ └── stats.html # 통계 대시볎드 +│ ├── pharmacy/ +│ │ ├── list.html # 앜국 목록 +│ │ ├── detail.html # 앜국 상섞 +│ │ ├── create.html # 앜국 등록 +│ │ └── edit.html # 앜국 수정 +│ ├── machines/ +│ │ ├── list.html # ëšžì‹  목록 (고도화) +│ │ ├── detail.html # ëšžì‹  상섞 (하드웚얎 정볎) +│ │ └── monitoring.html # 싀시간 몚니터링 +│ └── users/ +│ ├── list.html # 사용자 목록 (앜국 정볎 포핚) +│ └── detail.html # 사용자 상섞 +├── static/ +│ ├── css/ +│ │ ├── bootstrap.min.css # Bootstrap 5 +│ │ ├── custom.css # 컀슀텀 슀타음 +│ │ └── dashboard.css # 대시볎드 전용 슀타음 +│ ├── js/ +│ │ ├── bootstrap.min.js # Bootstrap JS +│ │ ├── chart.min.js # Chart.js 띌읎람러늬 +│ │ ├── dashboard.js # 대시볎드 JS +│ │ └── monitoring.js # 싀시간 몚니터링 JS +│ └── img/ +│ ├── logo.png # 팜큐 로고 +│ └── icons/ # 아읎윘듀 +├── utils/ +│ ├── __init__.py +│ ├── database.py # DB 연결 유틞늬티 +│ ├── auth.py # 읞슝 ꎀ렚 +│ ├── monitoring.py # 몚니터링 데읎터 수집 +│ └── proxmox.py # Proxmox API 연동 +└── docker/ + ├── Dockerfile # Flask 앱용 도컀파음 + └── docker-compose.yml # 통합 컚테읎너 구성 +``` + +## 🎚 UI/UX 섀계 + +### 디자읞 컚셉 +- **Modern Dashboard**: Bootstrap 5 êž°ë°˜ 반응형 디자읞 +- **팜큐 람랜딩**: 앜국 ꎀ늬에 특화된 색상/아읎윘 사용 +- **Korean-First**: 한국얎 우선 읞터페읎슀 +- **Mobile Responsive**: 몚바음/태랔늿 완벜 지원 + +### 메읞 대시볎드 레읎아웃 +``` +┌────────────────────────────────────────────────────────────┐ +│ 🏥 팜큐 앜국 ꎀ늬 시슀템 [ꎀ늬자: admin] [로귞아웃] │ +├───────────────────────────────────────────────────────────── +│ [대시볎드] [앜국ꎀ늬] [뚞신ꎀ늬] [사용자ꎀ늬] [몚니터링] [섀정] │ +├───────────────────────────────────────────────────────────── +│ 📊 전첎 현황 │ +│ ┌──────────┬──────────┬──────────┬──────────────────────┐ │ +│ │쎝 앜국 수 │옚띌읞 │였프띌읞 │평균 CPU 옚도 │ │ +│ │ 100 │ 95 │ 5 │ 62°C │ │ +│ └──────────┮──────────┮──────────┮──────────────────────┘ │ +│ │ +│ 🚚 싀시간 알늌 📈 성능 찚튞 │ +│ ┌─────────────────────────┐ ┌────────────────────┐ │ +│ │• 부산핎욎앜국: CPU 85°C │ │ [CPU 사용률 찚튞] │ │ +│ │• 대구쀑앙앜국: 디슀크95% │ │ [메몚늬 사용률] │ │ +│ │• 서욞앜국: 연결 끊김 │ │ [넀튞워크 튞래픜] │ │ +│ └─────────────────────────┘ └────────────────────┘ │ +│ │ +│ 📋 앜국별 상태 (싀시간) │ +│ ┌─────────────┬────────┬────────┬────────┬──────────────┐ │ +│ │앜국명 │상태 │CPU옚도 │메몚늬 │마지막 접속 │ │ +│ ├─────────────┌────────┌────────┌────────┌─────────────── │ +│ │서욞쀑앙앜국 │🟢 옚띌읞│ 65°C │ 80% │ 2분 전 │ │ +│ │부산핎욎앜국 │🟡 겜고 │ 85°C │ 60% │ 5분 전 │ │ +│ │대구쀑앙앜국 │🔎 위험 │ 70°C │ 95% │ 10분 전 │ │ +│ └─────────────┮────────┮────────┮────────┮──────────────┘ │ +└────────────────────────────────────────────────────────────┘ +``` + +## 🎯 핵심 Ʞ능 명섞 + +### 1. 통합 대시볎드 +```python +# routes/dashboard.py +@app.route('/') +def dashboard(): + stats = { + 'total_pharmacies': get_pharmacy_count(), + 'online_machines': get_online_machines_count(), + 'offline_machines': get_offline_machines_count(), + 'avg_cpu_temp': get_average_cpu_temperature(), + 'alerts': get_active_alerts(), + 'recent_activities': get_recent_activities() + } + return render_template('dashboard/index.html', stats=stats) +``` + +### 2. 앜국 ꎀ늬 시슀템 +#### 2-1. 앜국 목록 페읎지 +```html + +
+
+
+
+
+
🏥 앜국 ꎀ늬
+ +
+
+ + + + + + + + + + + + + + {% for pharmacy in pharmacies %} + + + + + + + + + + {% endfor %} + +
앜국명사업자번혞닎당자연결된 뚞신상태마지막 접속액션
+ {{ pharmacy.pharmacy_name }}
+ {{ pharmacy.address }} +
{{ pharmacy.business_number }} + {{ pharmacy.manager_name }}
+ {{ pharmacy.phone }} +
+ {{ pharmacy.machine_count }}대 + + {% if pharmacy.is_online %} + 🟢 옚띌읞 + {% else %} + 🔎 였프띌읞 + {% endif %} + {{ pharmacy.last_seen_humanized }} + +
+
+
+
+
+
+``` + +### 3. 고도화된 ëšžì‹  ꎀ늬 +#### 3-1. ëšžì‹  상섞 페읎지 (하드웚얎 정볎 포핚) +```html + +
+
+ +
+
+
+
🖥 ëšžì‹  Ʞ볞 정볎
+
+
+
+
뚞신명:
+
{{ machine.given_name }}
+ +
혞슀튞명:
+
{{ machine.hostname }}
+ +
IP 죌소:
+
+ {{ machine.ipv4 }} +
+ +
소속 앜국:
+
+ + {{ machine.pharmacy.pharmacy_name }} + +
+ +
마지막 접속:
+
+ {% if machine.is_online() %} + 🟢 옚띌읞 + {% else %} + 🔎 {{ machine.last_seen_humanized }} + {% endif %} +
+
+
+
+
+ + +
+
+
+
⚙ 하드웚얎 사양
+
+
+ {% if machine.specs %} +
+
CPU:
+
{{ machine.specs.cpu_model }} ({{ machine.specs.cpu_cores }}윔얎)
+ +
RAM:
+
{{ machine.specs.ram_gb }}GB
+ +
Storage:
+
{{ machine.specs.storage_gb }}GB
+ +
GPU:
+
{{ machine.specs.gpu_model or '없음' }}
+
+ {% else %} +

하드웚얎 정볎가 등록되지 않았습니닀.

+ + 하드웚얎 정볎 등록 + + {% endif %} +
+
+
+
+ + +
+
+
+
+
📊 싀시간 몚니터링
+
+
+ {% if machine.latest_monitoring %} +
+
+
+ +
CPU 사용률
+ {{ machine.latest_monitoring.cpu_usage }}% +
+
+
+
+ +
메몚늬 사용률
+ {{ machine.latest_monitoring.memory_usage }}% +
+
+
+
+
🌡
+
CPU 옚도
+ {{ machine.latest_monitoring.cpu_temperature }}°C +
+
+
+
+
💟
+
디슀크 사용률
+ {{ machine.latest_monitoring.disk_usage }}% +
+
+
+ {% else %} +
+ + 아직 몚니터링 데읎터가 없습니닀. 잠시 후 닀시 확읞핎죌섞요. +
+ {% endif %} +
+
+
+
+
+ + +``` + +### 4. 싀시간 몚니터링 시슀템 +```python +# routes/monitoring.py +from flask import Blueprint, jsonify +from utils.monitoring import collect_monitoring_data +from utils.proxmox import ProxmoxAPI + +monitoring_bp = Blueprint('monitoring', __name__) + +@monitoring_bp.route('/api/monitoring/realtime') +def realtime_monitoring(): + """싀시간 몚니터링 데읎터 API""" + data = { + 'total_machines': get_total_machines(), + 'online_count': get_online_machines_count(), + 'alerts': get_active_alerts(), + 'performance': get_performance_summary() + } + return jsonify(data) + +@monitoring_bp.route('/api/machines//monitoring') +def machine_monitoring(machine_id): + """특정 ëšžì‹  몚니터링 데읎터""" + monitoring_data = collect_monitoring_data(machine_id) + return jsonify(monitoring_data) +``` + +## 🔧 Ʞ술 슀택 + +### Backend +- **Flask 3.0**: 웹 프레임워크 +- **SQLAlchemy 2.0**: ORM (Ʞ졎 몚덞 재사용) +- **Jinja2**: 템플늿 엔진 +- **Flask-Login**: 사용자 읞슝 +- **APScheduler**: 백귞띌욎드 작업 (몚니터링 데읎터 수집) + +### Frontend +- **Bootstrap 5**: CSS 프레임워크 +- **Chart.js**: 찚튞 띌읎람러늬 +- **Font Awesome**: 아읎윘 +- **jQuery**: DOM 조작 +- **Socket.io**: 싀시간 통신 + +### 데읎터베읎슀 +- **SQLite**: Ʞ졎 Headscale DB 공유 +- **확장 테읎랔**: PharmacyInfo, MachineSpecs, MonitoringData + +### 배포 +- **Docker**: 컚테읎너화 +- **Nginx**: 늬버슀 프록시 (옵션) + +## 📅 개발 로드맵 + +### Phase 1: Ʞ볞 프레임워크 구축 (1-2음) +- [ ] Flask 애플늬쌀읎션 Ʞ볞 구조 생성 +- [ ] SQLAlchemy 몚덞 연동 (Ʞ졎 몚덞 재사용) +- [ ] Bootstrap êž°ë°˜ Ʞ볞 템플늿 구성 +- [ ] 띌우팅 구조 섀계 + +### Phase 2: 핵심 Ʞ능 구현 (3-4음) +- [ ] 메읞 대시볎드 구현 +- [ ] 앜국 ꎀ늬 CRUD Ʞ능 +- [ ] ëšžì‹  ꎀ늬 고도화 (하드웚얎 정볎 포핚) +- [ ] 사용자 ꎀ늬 확장 (앜국 정볎 연동) + +### Phase 3: 싀시간 Ʞ능 (2-3음) +- [ ] 몚니터링 데읎터 수집 시슀템 +- [ ] 싀시간 찚튞 및 알늌 +- [ ] WebSocket êž°ë°˜ 띌읎람 업데읎튞 +- [ ] Proxmox API 연동 + +### Phase 4: 통합 및 최적화 (2-3음) +- [ ] Ʞ졎 Headplane곌 데읎터 동Ʞ화 테슀튞 +- [ ] Docker 컚테읎너화 +- [ ] 성능 최적화 +- [ ] 사용자 테슀튞 및 플드백 반영 + +### Phase 5: 배포 및 욎영 (1-2음) +- [ ] Docker Compose 통합 구성 +- [ ] 프로덕션 배포 +- [ ] 몚니터링 및 로깅 섀정 +- [ ] 사용자 교육 자료 작성 + +## 💰 예상 늬소슀 + +### 개발 시간 +- **쎝 개발 êž°ê°„**: 8-12음 +- **개발자**: 1명 (풀타임) +- **음음 작업량**: 6-8시간 + +### Ʞ술적 요구사항 +- **Python 3.8+** +- **메몚늬**: 최소 512MB (Flask 앱) +- **디슀크**: 추가 100MB (정적 파음 포핚) + +## 🚀 시작하Ʞ + +### 1닚계: 개발 환겜 쀀비 +```bash +# Flask 프로젝튞 디렉터늬 생성 +mkdir farmq-admin +cd farmq-admin + +# Python 가상환겜 생성 +python3 -m venv flask-venv +source flask-venv/bin/activate + +# 필수 팚킀지 섀치 +pip install flask sqlalchemy jinja2 flask-login apscheduler +``` + +### 2닚계: Ʞ볞 구조 생성 +```bash +# 프로젝튞 구조 생성 +mkdir -p {routes,templates,static/{css,js,img},utils,models} +touch app.py config.py requirements.txt +``` + +### 3닚계: 첫 번짞 구현 +- Ʞ볞 Flask 앱 생성 +- SQLAlchemy 연동 +- 간닚한 대시볎드 페읎지 + +## 🎯 성공 지표 + +### Ʞ능적 목표 +- [ ] 100개 앜국 데읎터 완벜 ꎀ늬 +- [ ] 싀시간 몚니터링 정확도 95% 읎상 +- [ ] Ʞ졎 Headplane곌 데읎터 100% 동Ʞ화 +- [ ] 페읎지 로딩 시간 2쎈 읎낎 + +### 사용성 목표 +- [ ] ꎀ늬 업묎 횚윚성 70% 향상 +- [ ] 몚바음 접귌성 완벜 지원 +- [ ] 한국얎 UI 100% 완성 +- [ ] 사용자 만족도 4.8/5.0 읎상 + +읎제 읎 계획을 바탕윌로 Flask ꎀ늬자 페읎지 개발을 시작하시겠습니까? + +--- +**📅 작성음**: 2025-09-09 +**👀 작성자**: Claude Code Assistant +**🎯 목표**: Headplane UI 고도화륌 위한 Flask êž°ë°˜ ꎀ늬자 시슀템 \ No newline at end of file diff --git a/HEADSCALE_COMPLETE_GUIDE.md b/HEADSCALE_COMPLETE_GUIDE.md new file mode 100644 index 0000000..5dcd783 --- /dev/null +++ b/HEADSCALE_COMPLETE_GUIDE.md @@ -0,0 +1,424 @@ +# 🌐 FARMQ Headscale 완전 가읎드 + +## 📚 목찚 +1. [개념 정늬](#개념-정늬) +2. [아킀텍처 개요](#아킀텍처-개요) +3. [포튞 구성](#포튞-구성) +4. [섀치 및 구성](#섀치-및-구성) +5. [큎띌읎얞튞 연결 워크플로우](#큎띌읎얞튞-연결-워크플로우) +6. [싀제 사용 시나늬였](#싀제-사용-시나늬였) +7. [묞제 핎결](#묞제-핎결) + +--- + +## 🧠 개념 정늬 + +### Tailscale vs Headscale vs Headplane + +#### 1. **Tailscale** (원볞 서비슀) +- **정의**: 상용 VPN 서비슀 (SaaS) +- **특징**: + - 큎띌우드 êž°ë°˜ coordination server 사용 + - 구독 êž°ë°˜ 유료 서비슀 + - 자동화된 ꎀ늬 +- **닚점**: + - 데읎터가 왞부 서버륌 거칚 + - 비용 발생 + - 프띌읎버시 ìš°ë € + +#### 2. **Headscale** (였픈소슀 서버) +- **정의**: Tailscale의 coordination server륌 대첎하는 였픈소슀 구현 +- **특징**: + - 자첎 혞슀팅 가능 + - 완전한 프띌읎버시 제얎 + - 묎료 사용 + - REST API 제공 +- **역할**: + - 큎띌읎얞튞 읞슝 및 등록 + - IP 죌소 할당 + - 띌우팅 테읎랔 ꎀ늬 + - í‚€ 교환 coordination + +#### 3. **Headplane** (웹 UI) +- **정의**: Headscale을 ꎀ늬하Ʞ 위한 웹 읞터페읎슀 +- **특징**: + - 람띌우저에서 녾드 ꎀ늬 + - 시각적 넀튞워크 상태 확읞 + - 사용자 및 í‚€ ꎀ늬 + +#### 4. **큎띌읎얞튞 (Tailscale 큎띌읎얞튞)** +- **정의**: 싀제 VPN 연결을 닎당하는 큎띌읎얞튞 프로귞랚 +- **쀑요**: Tailscale의 **큎띌읎얞튞 소프튞웚얎**륌 귞대로 사용 +- **변겜점**: 서버 죌소만 Headscale 서버로 지정 + +--- + +## 🏗 아킀텍처 개요 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 큎띌읎얞튞 PC │ │ Headscale │ │ 큎띌읎얞튞 PC │ +│ │ │ 서버 │ │ │ +│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ +│ │ Tailscale │◄────┌── Headscale │─┌───── │ Tailscale │ │ +│ │ Client │ │ │ │ Server │ │ │ │ Client │ │ +│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ +│ │ │ ┌─────────────┐ │ │ │ +│ │ │ │ Headplane │ │ │ │ +│ │ │ │ Web UI │ │ │ │ +│ │ │ └─────────────┘ │ │ │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └────────────────────────┌────────────────────────┘ + │ + ┌─────────────────┐ + │ FARMQ Flask │ + │ Admin Panel │ + └─────────────────┘ +``` + +### 데읎터 흐멄 +1. **등록**: 큎띌읎얞튞 → Headscale 서버 (읞슝) +2. **í‚€ 교환**: Headscale 서버 → 큎띌읎얞튞듀 (P2P í‚€ 정볎) +3. **싀제 통신**: 큎띌읎얞튞 ↔ 큎띌읎얞튞 (직접 P2P) +4. **ꎀ늬**: Headplane/Flask UI → Headscale API + +--- + +## 🔌 포튞 구성 + +### 현재 FARMQ 섀정 + +| 서비슀 | 포튞 | 프로토윜 | 용도 | ì ‘ê·Œ | +|--------|------|----------|------|------| +| **Headscale Server** | `8070` | HTTP | 큎띌읎얞튞 등록/ꎀ늬 | 큎띌읎얞튞 ← → 서버 | +| **Headplane UI** | `3000` | HTTP | 웹 ꎀ늬 읞터페읎슀 | ꎀ늬자 → 웹람띌우저 | +| **FARMQ Admin** | `5001` | HTTP | 한국얎 ꎀ늬 페읎지 | ꎀ늬자 → 웹람띌우저 | + +### 쀑요한 포읞튞 +- **큎띌읎얞튞가 사용하는 포튞**: `8070` (Headscale 서버) +- **ꎀ늬자가 사용하는 포튞**: `3000` (Headplane), `5001` (FARMQ Admin) +- **낎부 컚테읎너 포튞**: `8080` (Docker 낎부에서만 사용) + +--- + +## 🛠 섀치 및 구성 + +### 1. 서버 구성 (읎믞 완료) + +```bash +# 서버 시작 +cd /srv/headscale-setup +docker-compose up -d + +# 서비슀 확읞 +docker-compose ps +``` + +### 2. 큎띌읎얞튞 섀치 곌정 + +#### Step 1: Tailscale 큎띌읎얞튞 섀치 + +```bash +# Ubuntu/Debian +curl -fsSL https://tailscale.com/install.sh | sh + +# 또는 수동 섀치 +curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/$(lsb_release -cs).noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null +curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/$(lsb_release -cs).tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list +sudo apt update +sudo apt install tailscale +``` + +#### Step 2: Headscale 서버에 등록 + +```bash +# Ʞ볞 명령얎 형식 +sudo tailscale up --login-server=http://[서버IP]:8070 --authkey=[PreAuthí‚€] + +# FARMQ 싀제 명령얎 예시 +sudo tailscale up \ + --login-server=http://192.168.0.151:8070 \ + --authkey=YOUR_PREAUTH_KEY_HERE \ + --hostname=pharmacy-busan-pc1 \ + --accept-dns=false +``` + +--- + +## 🔄 큎띌읎얞튞 연결 워크플로우 + +### 전첎 프로섞슀 + +```mermaid +sequenceDiagram + participant Admin as ꎀ늬자 + participant Server as Headscale 서버 + participant Client as 큎띌읎얞튞 PC + participant Network as VPN 넀튞워크 + + Admin->>Server: 1. 사용자 생성 + Admin->>Server: 2. PreAuth í‚€ 생성 + Admin->>Client: 3. PreAuth í‚€ 전달 + Client->>Client: 4. Tailscale 큎띌읎얞튞 섀치 + Client->>Server: 5. 등록 요청 (PreAuth í‚€ 포핚) + Server->>Client: 6. 읞슝 완료 및 섀정 전달 + Client->>Network: 7. VPN 넀튞워크 ì°žì—¬ + Network->>Client: 8. 닀륞 녞드듀곌 P2P 연결 +``` + +### 상섞 닚계별 섀명 + +#### 1. 서버 ìž¡ 작업 (ꎀ늬자) + +```bash +# 1-1. 사용자 생성 (한 번만) +docker exec headscale headscale users create pharmacy-busan + +# 1-2. 사용자 목록 확읞 +docker exec headscale headscale users list + +# 1-3. PreAuth í‚€ 생성 +docker exec headscale headscale preauthkeys create --user 1 --expiration 1h + +# 출력 예시: +# f8d9c7e4b2a6c8f4d9e7b3a5c8f4d9e7b3a5c8f4d9e7b3a5c8f4d9e7b3a5c8f4 +``` + +#### 2. 큎띌읎얞튞 ìž¡ 작업 + +```bash +# 2-1. Ʞ졎 연결 핎제 (있닀멎) +sudo tailscale logout +sudo tailscale down + +# 2-2. Headscale 서버에 등록 +sudo tailscale up \ + --login-server=http://192.168.0.151:8070 \ + --authkey=f8d9c7e4b2a6c8f4d9e7b3a5c8f4d9e7b3a5c8f4d9e7b3a5c8f4d9e7b3a5c8f4 \ + --hostname=pharmacy-busan-pc1 \ + --accept-dns=false + +# 2-3. 연결 상태 확읞 +tailscale status + +# 2-4. IP 죌소 확읞 +tailscale ip -4 +``` + +#### 3. 결곌 확읞 + +```bash +# 서버에서 녾드 목록 확읞 +docker exec headscale headscale nodes list + +# 웹 UI에서 확읞 +# - Headplane: http://192.168.0.151:3000 +# - FARMQ Admin: http://192.168.0.151:5001 +``` + +--- + +## 🏥 싀제 사용 시나늬였 + +### 시나늬였 1: 새 앜국 등록 + +```bash +# 서버 작업 (ꎀ늬자) +docker exec headscale headscale users create pharmacy-seoul +docker exec headscale headscale preauthkeys create --user [USER_ID] --expiration 2h + +# 큎띌읎얞튞 작업 (앜국 PC) +curl -O http://192.168.0.151:8000/add-client.sh +chmod +x add-client.sh +./add-client.sh pharmacy-seoul pos-terminal-1 +# PreAuth í‚€ 입력 시 위에서 생성한 í‚€ 사용 +``` + +### 시나늬였 2: 여러 PC가 있는 앜국 + +```bash +# 같은 사용자로 여러 ëšžì‹  등록 가능 +sudo tailscale up --login-server=http://192.168.0.151:8070 --authkey=[KEY] --hostname=busan-pos1 +sudo tailscale up --login-server=http://192.168.0.151:8070 --authkey=[KEY] --hostname=busan-pos2 +sudo tailscale up --login-server=http://192.168.0.151:8070 --authkey=[KEY] --hostname=busan-office +``` + +### 시나늬였 3: 임시 접속 (녞튞북) + +```bash +# 짧은 만료시간윌로 í‚€ 생성 +docker exec headscale headscale preauthkeys create --user [USER_ID] --expiration 30m + +# 녞튞북에서 임시 연결 +sudo tailscale up --login-server=http://192.168.0.151:8070 --authkey=[TEMP_KEY] --hostname=manager-laptop +``` + +--- + +## 🔍 상태 확읞 및 ꎀ늬 + +### 명령얎 몚음 + +```bash +# === 서버 ìž¡ (Headscale) === + +# 사용자 ꎀ늬 +docker exec headscale headscale users list +docker exec headscale headscale users create [username] + +# 녾드 ꎀ늬 +docker exec headscale headscale nodes list +docker exec headscale headscale nodes expire [node_id] + +# PreAuth í‚€ ꎀ늬 +docker exec headscale headscale preauthkeys list --user [user_id] +docker exec headscale headscale preauthkeys create --user [user_id] --expiration [time] + +# === 큎띌읎얞튞 ìž¡ (Tailscale) === + +# 상태 확읞 +tailscale status # 넀튞워크 상태 및 연결된 녞드듀 +tailscale ip # 낮 IP 죌소듀 +tailscale netcheck # 넀튞워크 연결성 테슀튞 +tailscale ping [node] # 특정 녾드 ping + +# 연결 ꎀ늬 +tailscale up # 연결 시작 +tailscale down # 연결 쀑닚 +tailscale logout # 로귞아웃 + +# 로귞 확읞 +sudo journalctl -u tailscaled -f +``` + +### 웹 UI ì ‘ê·Œ + +```bash +# Headplane (Ʞ볞 ꎀ늬 UI) +http://192.168.0.151:3000 +# API í‚€: 8qRr1IB.tV95CmA0fLaCiGGIgBfeoN9daHceFkzI + +# FARMQ ꎀ늬자 페읎지 (한국얎) +http://192.168.0.151:5001 +``` + +--- + +## ⚠ 쀑요한 죌의사항 + +### 1. 포튞 혌동 방지 +- **큎띌읎얞튞 등록**: `8070` 포튞 사용 +- **웹 ꎀ늬**: `3000`, `5001` 포튞 사용 +- **Docker 낎부**: `8080` 포튞 (왞부에서 직접 ì ‘ê·Œ 불가) + +### 2. PreAuth í‚€ 볎안 +- 킀는 음회용 또는 제한된 횟수만 사용 가능 +- 짧은 만료시간 섀정 권장 (1h ~ 24h) +- í‚€ ë…žì¶œ 시 슉시 새 í‚€ 생성 + +### 3. 넀튞워크 구성 +- 몚든 큎띌읎얞튞는 `100.64.0.0/10` 대역 IP 할당 +- 첫 번짞 큎띌읎얞튞: `100.64.0.1` (서버 역할도 핹) +- 읎후 큎띌읎얞튞듀: `100.64.0.2`, `100.64.0.3`, ... + +### 4. 방화벜 섀정 +```bash +# 서버 ìž¡ 방화벜 (필요시) +sudo ufw allow 8070/tcp # Headscale +sudo ufw allow 3000/tcp # Headplane +sudo ufw allow 5001/tcp # FARMQ Admin + +# 큎띌읎얞튞 ìž¡ (Tailscale읎 자동 처늬) +sudo ufw allow in on tailscale0 +``` + +--- + +## 🚚 묞제 핎결 + +### 음반적읞 묞제듀 + +#### 1. "connection refused" 였류 +```bash +# 원읞: 서버 포튞 ì ‘ê·Œ 불가 +# 핎결: +docker-compose ps # 서버 싀행 확읞 +sudo ufw status # 방화벜 확읞 +ping 192.168.0.151 # 서버 연결 확읞 +``` + +#### 2. "invalid auth key" 였류 +```bash +# 원읞: PreAuth í‚€ 만료 또는 잘못된 í‚€ +# 핎결: +docker exec headscale headscale preauthkeys create --user [user_id] --expiration 1h +``` + +#### 3. "user not found" 였류 +```bash +# 원읞: 졎재하지 않는 사용자 +# 핎결: +docker exec headscale headscale users list # 사용자 확읞 +docker exec headscale headscale users create [username] # 사용자 생성 +``` + +#### 4. IP 할당되지 않음 +```bash +# 진닚: +tailscale status # 연결 상태 확읞 +tailscale netcheck # 넀튞워크 테슀튞 +sudo journalctl -u tailscaled -f # 로귞 확읞 + +# 핎결: +sudo systemctl restart tailscaled # 서비슀 재시작 +``` + +### 로귞 위치 +```bash +# Headscale 서버 로귞 +docker logs headscale + +# Headplane 로귞 +docker logs headplane + +# Tailscale 큎띌읎얞튞 로귞 +sudo journalctl -u tailscaled -f +``` + +--- + +## 📋 첎크늬슀튞 + +### 서버 섀치 완료 확읞 +- [ ] Docker 및 Docker Compose 섀치됚 +- [ ] Headscale 컚테읎너 싀행 쀑 (포튞 8070) +- [ ] Headplane 컚테읎너 싀행 쀑 (포튞 3000) +- [ ] FARMQ Admin 싀행 쀑 (포튞 5001) +- [ ] 방화벜에서 필요 포튞 엎늌 + +### 큎띌읎얞튞 연결 확읞 +- [ ] Tailscale 큎띌읎얞튞 섀치됚 +- [ ] 서버에 사용자 생성됚 +- [ ] PreAuth í‚€ 생성됚 (유횚한 만료시간) +- [ ] `tailscale up` 명령얎 성공 +- [ ] `tailscale status`에서 닀륞 녞드듀 볎임 +- [ ] VPN IP 죌소 할당됚 (`100.64.0.x`) + +### 넀튞워크 연결 확읞 +- [ ] 서버 ping 성공 (`ping 100.64.0.1`) +- [ ] 닀륞 큎띌읎얞튞와 ping 성공 +- [ ] 웹 UI ì ‘ê·Œ 가능 + +--- + +## 🎯 닀음 닚계 + +1. **자동화 슀크늜튞 활용**: `add-client.sh`, `create-preauth-key.sh` 사용 +2. **몚니터링 섀정**: FARMQ Admin에서 싀시간 상태 확읞 +3. **백업 전략**: Headscale 섀정 및 데읎터베읎슀 백업 +4. **확장**: 새로욎 앜국 및 지점 추가 + +--- + +**🎊 읎제 완전한 프띌읎빗 VPN 넀튞워크륌 욎영할 수 있습니닀!** \ No newline at end of file diff --git a/PREAUTH_KEY_MANAGEMENT_GUIDE.md b/PREAUTH_KEY_MANAGEMENT_GUIDE.md new file mode 100644 index 0000000..ee85810 --- /dev/null +++ b/PREAUTH_KEY_MANAGEMENT_GUIDE.md @@ -0,0 +1,480 @@ +# 🔑 FARMQ Headscale Pre-auth Key ꎀ늬 가읎드 + +## 📚 목찚 +1. [Pre-auth Key 개념](#pre-auth-key-개념) +2. [í‚€ 유형별 비교](#í‚€-유형별-비교) +3. [앜국 환겜별 사용 전략](#앜국-환겜별-사용-전략) +4. [싀제 명령얎 예시](#싀제-명령얎-예시) +5. [볎안 ꎀ늬](#볎안-ꎀ늬) +6. [묞제 핎결](#묞제-핎결) +7. [첎크늬슀튞](#첎크늬슀튞) + +--- + +## 🧠 Pre-auth Key 개념 + +### Pre-auth Key란? +- **사전 읞슝 í‚€**: 큎띌읎얞튞가 Headscale 서버에 자동 등록할 수 있는 "입장권" +- **음회용 팚슀워드** 개념윌로, ꎀ늬자가 믞늬 생성핎서 배포 +- **볎안 계잵**: 묎작위 접속을 방지하는 첫 번짞 볎안 장벜 + +### 작동 원늬 +```mermaid +sequenceDiagram + participant Admin as ꎀ늬자 + participant Server as Headscale 서버 + participant Client as 큎띌읎얞튞 + + Admin->>Server: 1. PreAuth í‚€ 생성 + Server-->>Admin: 2. í‚€ 반환 (abc123def456...) + Admin->>Client: 3. í‚€ 전달 + Client->>Server: 4. 킀와 핚께 등록 요청 + Server->>Server: 5. í‚€ 검슝 + Server-->>Client: 6. 승읞 및 VPN 섀정 전송 + Server->>Server: 7. í‚€ 사용됚 표시 (음회용읞 겜우) +``` + +--- + +## 🔄 í‚€ 유형별 비교 + +### 1. 음회용 í‚€ (Single-use Key) +```bash +# 생성 명령얎 +docker exec headscale headscale preauthkeys create --user [USER_ID] --expiration 1h + +# 특징 +✅ 최고 수쀀 볎안 +✅ 정확한 êž°êž° 추적 가능 +❌ 맀번 새 í‚€ 생성 필요 +❌ ꎀ늬 복잡도 높음 +``` + +### 2. 재사용 í‚€ (Reusable Key) +```bash +# 생성 명령얎 +docker exec headscale headscale preauthkeys create --user [USER_ID] --expiration 7d --reusable + +# 특징 +✅ 펞늬한 ꎀ늬 +✅ 여러 ꞰꞰ에서 동음 í‚€ 사용 +⚠ í‚€ ë…žì¶œ 시 볎안 위험 +⚠ ꞰꞰ별 구분 얎렀움 +``` + +### 3. 임시 í‚€ (Ephemeral Key) +```bash +# 생성 명령얎 +docker exec headscale headscale preauthkeys create --user [USER_ID] --expiration 30m --ephemeral + +# 특징 +✅ 음시적 접속용 최적 +✅ 넀튞워크에서 자동 제거 +❌ 영구 연결 불가 +❌ 재시작 시 재등록 필요 +``` + +--- + +## 🏥 앜국 환겜별 사용 전략 + +### 전략 1: 앜국별 개별 í‚€ (🌟 권장) + +#### 적용 대상 +- 정Ʞ적윌로 욎영되는 앜국 +- 여러 POS 닚말Ʞ가 있는 맀장 +- 볎안읎 쀑요한 환겜 + +#### 섀정 예시 +```bash +# 1닚계: 앜국별 사용자 생성 +docker exec headscale headscale users create pharmacy-gangnam +docker exec headscale headscale users create pharmacy-hongdae +docker exec headscale headscale users create pharmacy-itaewon + +# 2닚계: 사용자 ID 확읞 +docker exec headscale headscale users list +# 출력: +# ID | Name +# 1 | myuser +# 2 | pharmacy-gangnam +# 3 | pharmacy-hongdae +# 4 | pharmacy-itaewon + +# 3닚계: 앜국별 재사용 í‚€ 생성 +docker exec headscale headscale preauthkeys create --user 2 --expiration 30d --reusable +# 강낚앜국용: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0 + +docker exec headscale headscale preauthkeys create --user 3 --expiration 30d --reusable +# 홍대앜국용: z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2h1g0f9e8d7c6b5a4z3y2x1w0 + +docker exec headscale headscale preauthkeys create --user 4 --expiration 30d --reusable +# 읎태원앜국용: m5n6o7p8q9r0s1t2u3v4w5x6y7z8a9b0c1d2e3f4g5h6i7j8k9l0m1n2o3p4 +``` + +#### 장점 +- ✅ **앜국별 구분**: 넀튞워크에서 앜국별로 명확히 구분 +- ✅ **부분적 볎안**: 한 앜국의 í‚€ 녞출읎 닀륞 앜국에 영향 없음 +- ✅ **ꎀ늬 용읎**: 앜국별로 í‚€ 갱신 및 ꎀ늬 가능 +- ✅ **확장성**: 새 앜국 추가 시 독늜적윌로 ꎀ늬 + +### 전략 2: 지역별 귞룹 í‚€ + +#### 적용 대상 +- 같은 지역 낮 여러 지점 +- ꎀ늬 구역별 분할 필요 시 +- 쀑간 규몚 볎안 요구사항 + +```bash +# 지역별 사용자 생성 +docker exec headscale headscale users create region-seoul +docker exec headscale headscale users create region-busan +docker exec headscale headscale users create region-daegu + +# 지역별 í‚€ 생성 (서욞 지역 몚든 앜국읎 공유) +docker exec headscale headscale preauthkeys create --user 2 --expiration 14d --reusable +``` + +### 전략 3: 닚음 공통 í‚€ (⚠ 비권장) + +#### 적용 대상 +- 테슀튞 환겜 +- 맀우 소규몚 욎영 (5개 믞만 앜국) +- ꎀ늬 늬소슀 극도로 제한적읞 겜우 + +```bash +# 몚든 앜국읎 하나의 í‚€ 공유 +docker exec headscale headscale preauthkeys create --user 1 --expiration 90d --reusable +``` + +#### 닚점 +- ❌ **볎안 위험**: í‚€ 하나만 녞출되멎 전첎 넀튞워크 위험 +- ❌ **ꎀ늬 복잡**: 묞제 발생 시 원읞 추적 얎렀움 +- ❌ **확장성 부족**: 규몚 슝가 시 ꎀ늬 한계 + +--- + +## 💻 싀제 명령얎 예시 + +### FARMQ 표쀀 섀정 (권장) + +#### 1닚계: 앜국 등록 쀀비 +```bash +# 새 앜국 등록 시 싀행할 명령얎듀 + +# 앜국명 변수 섀정 (펞의륌 위핎) +PHARMACY_NAME="pharmacy-myeongdong" +EXPIRATION="30d" # 30음 만료 + +echo "🏥 새 앜국 등록: $PHARMACY_NAME" +``` + +#### 2닚계: 사용자 생성 +```bash +# 사용자 생성 +docker exec headscale headscale users create "$PHARMACY_NAME" + +# 생성 결곌 확읞 +docker exec headscale headscale users list +``` + +#### 3닚계: 사용자 ID 확읞 +```bash +# 방법 1: 수동 확읞 +docker exec headscale headscale users list | grep "$PHARMACY_NAME" + +# 방법 2: 자동 추출 (슀크늜튞용) +USER_ID=$(docker exec headscale headscale users list | grep "$PHARMACY_NAME" | awk '{print $1}') +echo "사용자 ID: $USER_ID" +``` + +#### 4닚계: Pre-auth í‚€ 생성 +```bash +# 재사용 가능한 í‚€ 생성 +PREAUTH_KEY=$(docker exec headscale headscale preauthkeys create --user "$USER_ID" --expiration "$EXPIRATION" --reusable | tail -1) + +echo "🔑 생성된 Pre-auth Key:" +echo "$PREAUTH_KEY" +``` + +#### 5닚계: 큎띌읎얞튞에서 사용 +```bash +# 앜국의 각 ꞰꞰ에서 싀행 +sudo tailscale up \ + --login-server=http://192.168.0.151:8070 \ + --authkey="$PREAUTH_KEY" \ + --hostname=myeongdong-pos1 \ + --accept-dns=false + +sudo tailscale up \ + --login-server=http://192.168.0.151:8070 \ + --authkey="$PREAUTH_KEY" \ + --hostname=myeongdong-pos2 \ + --accept-dns=false + +sudo tailscale up \ + --login-server=http://192.168.0.151:8070 \ + --authkey="$PREAUTH_KEY" \ + --hostname=myeongdong-office \ + --accept-dns=false +``` + +### 특수 상황별 명령얎 + +#### 임시 접속 (맀니저 녞튞북) +```bash +# 2시간 짜늬 음회용 í‚€ +docker exec headscale headscale preauthkeys create --user "$USER_ID" --expiration 2h + +# 음시적 접속 (재부팅 시 자동 핎제) +docker exec headscale headscale preauthkeys create --user "$USER_ID" --expiration 1h --ephemeral +``` + +#### Ʞ술 지원용 (원격 지원) +```bash +# 30분 짜늬 ephemeral í‚€ (지원 완료 후 자동 삭제) +docker exec headscale headscale preauthkeys create --user "$USER_ID" --expiration 30m --ephemeral +``` + +#### 테슀튞용 (개발/검슝) +```bash +# 테슀튞 사용자 및 짧은 만료시간 +docker exec headscale headscale users create test-environment +docker exec headscale headscale preauthkeys create --user [TEST_USER_ID] --expiration 15m --reusable +``` + +--- + +## 🔐 볎안 ꎀ늬 + +### í‚€ 생명죌Ʞ ꎀ늬 + +#### 1. í‚€ 생성 정책 +```bash +# 권장 만료시간 섀정 +# - 음반 앜국: 30음 +# - 임시 접속: 2-8시간 +# - Ʞ술 지원: 30분-1시간 +# - 테슀튞: 15분-1시간 + +# 예시: 닚계별 만료시간 +docker exec headscale headscale preauthkeys create --user 2 --expiration 30d --reusable # 욎영 +docker exec headscale headscale preauthkeys create --user 2 --expiration 4h # 임시 +docker exec headscale headscale preauthkeys create --user 2 --expiration 30m --ephemeral # 지원 +``` + +#### 2. í‚€ 갱신 슀쌀쀄 +```bash +# 월별 í‚€ 갱신 슀크늜튞 (cron 등록 권장) +#!/bin/bash +# monthly-key-renewal.sh + +PHARMACIES=("pharmacy-gangnam" "pharmacy-hongdae" "pharmacy-itaewon") + +for pharmacy in "${PHARMACIES[@]}"; do + echo "🔄 갱신 쀑: $pharmacy" + + # Ʞ졎 í‚€ 만료 처늬 (수동) + echo "⚠ Ʞ졎 킀륌 수동윌로 비활성화하섞요" + + # 새 í‚€ 생성 + USER_ID=$(docker exec headscale headscale users list | grep "$pharmacy" | awk '{print $1}') + NEW_KEY=$(docker exec headscale headscale preauthkeys create --user "$USER_ID" --expiration 30d --reusable | tail -1) + + echo "🔑 $pharmacy 새 í‚€: $NEW_KEY" + echo "📧 앜국에 새 í‚€ 전달 필요" +done +``` + +#### 3. í‚€ 몚니터링 +```bash +# 활성 í‚€ 확읞 +docker exec headscale headscale preauthkeys list --user [USER_ID] + +# 만료 예정 í‚€ 확읞 (슀크늜튞화 권장) +docker exec headscale headscale preauthkeys list --user [USER_ID] | grep -E "(expires|expired)" +``` + +### 볎안 사고 대응 + +#### í‚€ ë…žì¶œ 시 대응 절찚 +```bash +# 1닚계: 슉시 새 í‚€ 생성 +EMERGENCY_KEY=$(docker exec headscale headscale preauthkeys create --user [USER_ID] --expiration 7d --reusable | tail -1) + +# 2닚계: 핎당 앜국에 ꞎ꞉ 연띜 +echo "🚚 ꞎ꞉ í‚€ 교첎 필요" +echo "새 í‚€: $EMERGENCY_KEY" + +# 3닚계: Ʞ졎 킀로 등록된 녾드 확읞 +docker exec headscale headscale nodes list --user [USER_ID] + +# 4닚계: 의심슀러욎 녾드 제거 (필요시) +# docker exec headscale headscale nodes delete [NODE_ID] +``` + +### ì ‘ê·Œ 제한 섀정 + +#### 태귞 êž°ë°˜ ì ‘ê·Œ 제얎 (고꞉) +```bash +# 앜국별 태귞 섀정 +docker exec headscale headscale preauthkeys create \ + --user [USER_ID] \ + --expiration 30d \ + --reusable \ + --tags "pharmacy:gangnam,role:pos" + +# 지역별 ì ‘ê·Œ 제한 +docker exec headscale headscale preauthkeys create \ + --user [USER_ID] \ + --expiration 30d \ + --reusable \ + --tags "region:seoul,type:retail" +``` + +--- + +## 🔧 묞제 핎결 + +### 음반적읞 묞제듀 + +#### 1. "invalid auth key" 였류 +```bash +# 원읞: í‚€ 만료, 잘못된 í‚€, 읎믞 사용된 음회용 í‚€ +# 진닚: +docker exec headscale headscale preauthkeys list --user [USER_ID] + +# 핎결: 새 í‚€ 생성 +docker exec headscale headscale preauthkeys create --user [USER_ID] --expiration 1h --reusable +``` + +#### 2. "user not found" 였류 +```bash +# 원읞: 졎재하지 않는 사용자 ID +# 진닚: +docker exec headscale headscale users list + +# 핎결: 사용자 생성 +docker exec headscale headscale users create [USERNAME] +``` + +#### 3. "foreign key constraint" 였류 +```bash +# 원읞: 데읎터베읎슀 묎결성 묞제 (FARMQ 확장 테읎랔곌 충돌) +# 핎결: Ʞ졎 사용자 사용 또는 데읎터베읎슀 정늬 +docker exec headscale headscale users list # Ʞ졎 사용자 확읞 +# Ʞ졎 사용자 ID로 í‚€ 생성 +``` + +### 디버깅 명령얎 +```bash +# 전첎 í‚€ 목록 확읞 +docker exec headscale headscale preauthkeys list + +# 특정 사용자의 í‚€ 목록 +docker exec headscale headscale preauthkeys list --user [USER_ID] + +# 녾드 등록 상태 확읞 +docker exec headscale headscale nodes list + +# 로귞 확읞 +docker logs headscale | grep -i "preauth\|auth\|key" +``` + +--- + +## 📋 첎크늬슀튞 + +### 새 앜국 등록 첎크늬슀튞 +- [ ] 앜국명 결정 (naming convention 쀀수) +- [ ] Headscale 사용자 생성 +- [ ] 사용자 ID 확읞 +- [ ] 적절한 만료시간윌로 Pre-auth í‚€ 생성 +- [ ] 킀륌 안전한 방법윌로 앜국에 전달 +- [ ] 앜국에서 큎띌읎얞튞 등록 테슀튞 +- [ ] 넀튞워크 연결 확읞 +- [ ] FARMQ ꎀ늬자 페읎지에서 확읞 + +### ì •êž° 볎안 점검 첎크늬슀튞 +- [ ] 만료 예정 í‚€ 확읞 (30음 전 알늌) +- [ ] 사용되지 않는 í‚€ 정늬 +- [ ] 의심슀러욎 녾드 연결 확읞 +- [ ] í‚€ 사용 로귞 검토 +- [ ] 백업된 í‚€ 정볎 업데읎튞 + +### ꞎ꞉ 상황 대응 첎크늬슀튞 +- [ ] í‚€ ë…žì¶œ 확읞 시 슉시 새 í‚€ 생성 +- [ ] 핎당 앜국에 ꞎ꞉ 연띜 +- [ ] 의심슀러욎 녾드 찚닚 +- [ ] 사고 겜위 묞서화 +- [ ] 재발 방지 대책 수늜 + +--- + +## 📚 명령얎 ì°žì¡° 칎드 + +### 자죌 사용하는 명령얎 +```bash +# === 사용자 ꎀ늬 === +docker exec headscale headscale users create [USERNAME] +docker exec headscale headscale users list + +# === í‚€ 생성 === +# 음회용 +docker exec headscale headscale preauthkeys create --user [USER_ID] --expiration 1h + +# 재사용 (음반적) +docker exec headscale headscale preauthkeys create --user [USER_ID] --expiration 30d --reusable + +# 임시 (ephemeral) +docker exec headscale headscale preauthkeys create --user [USER_ID] --expiration 30m --ephemeral + +# === í‚€ ꎀ늬 === +docker exec headscale headscale preauthkeys list --user [USER_ID] +docker exec headscale headscale preauthkeys expire [KEY_ID] + +# === 녾드 ꎀ늬 === +docker exec headscale headscale nodes list +docker exec headscale headscale nodes list --user [USER_ID] +docker exec headscale headscale nodes delete [NODE_ID] +``` + +### 큎띌읎얞튞 명령얎 +```bash +# 표쀀 등록 +sudo tailscale up \ + --login-server=http://192.168.0.151:8070 \ + --authkey=[PREAUTH_KEY] \ + --hostname=[HOSTNAME] \ + --accept-dns=false + +# 상태 확읞 +tailscale status +tailscale ip -4 + +# 연결 핎제 +sudo tailscale down +sudo tailscale logout +``` + +--- + +## 🎯 몚범 사례 요앜 + +### DO ✅ +- **앜국별 개별 í‚€ 사용** +- **적절한 만료시간 섀정** (30음 권장) +- **정Ʞ적읞 í‚€ 갱신** +- **í‚€ 전달 시 볎안 채널 사용** +- **í‚€ 사용 로귞 몚니터링** + +### DON'T ❌ +- **몚든 앜국읎 하나의 í‚€ 공유하지 않Ʞ** +- **만료시간 너묎 Ꞟ게 섀정하지 않Ʞ** (90음 읎상) +- **킀륌 평묞윌로 읎메음 전송하지 않Ʞ** +- **만료된 í‚€ 방치하지 않Ʞ** +- **í‚€ 백업 없읎 욎영하지 않Ʞ** + +--- + +**🎊 첎계적읞 í‚€ ꎀ늬로 안전한 FARMQ 넀튞워크륌 욎영하섞요!** \ No newline at end of file diff --git a/PROXMOX_VNC_INTEGRATION_PLAN.md b/PROXMOX_VNC_INTEGRATION_PLAN.md new file mode 100644 index 0000000..552660a --- /dev/null +++ b/PROXMOX_VNC_INTEGRATION_PLAN.md @@ -0,0 +1,376 @@ +# 팜큐(FARMQ) Proxmox VNC 통합 시슀템 Ʞ획서 + +## 🎯 프로젝튞 개요 + +### 목표 +Flask Admin 웹 읞터페읎슀에서 **한 번의 큎늭**윌로 Proxmox VM의 VNC 화멎에 접속할 수 있는 통합 시슀템 구축 + +### 핵심 아읎디얎 +1. **Headscale 넀튞워크**륌 통핎 몚든 앜국의 Proxmox 혞슀튞에 ì ‘ê·Œ 가능 +2. **Proxmox VNC API**륌 활용하여 VM 화멎 원격 제얎 +3. **람띌우저 êž°ë°˜ VNC 큎띌읎얞튞** (Guacamole/noVNC)로 슉시 접속 +4. **Flask Admin 버튌** → **VNC 화멎** 원큎늭 연결 + +## 🏗 시슀템 아킀텍처 + +``` +[Flask Admin Dashboard] + ↓ (큎늭) +[VNC 연결 요청 API] + ↓ +[Headscale 넀튞워크] + ↓ (100.64.0.x) +[Proxmox Host Server] + ↓ (VNC API) +[VM VNC Console] + ↓ +[noVNC/Guacamole Web Client] + ↓ +[사용자 람띌우저] +``` + +## 📋 Ʞ술 슀택 + +### Frontend +- **noVNC**: HTML5 VNC 큎띌읎얞튞 (가벌움, 쉬욎 통합) +- **Apache Guacamole**: 더 고꞉ Ʞ능 (큎늜볎드, 파음 전송) +- **Bootstrap 5**: UI 프레임워크 + +### Backend +- **Flask**: 웹 서버 및 API +- **Proxmox VE API**: VM ꎀ늬 및 VNC 토큰 생성 +- **WebSocket Proxy**: VNC 튞래픜 쀑계 + +### Network +- **Headscale**: 팜큐 넀튞워크 (100.64.0.0/10) +- **Tailscale**: 각 Proxmox 혞슀튞 연결 + +## 🔧 구현 닚계 + +### Phase 1: Proxmox API 통합 (1-2음) + +#### 1.1 Proxmox API 큎띌읎얞튞 구현 +```python +# utils/proxmox_client.py +class ProxmoxClient: + def __init__(self, host, username, password): + self.host = host # 100.64.0.x (Headscale IP) + + def get_vm_list(self): + """VM 목록 조회""" + + def get_vnc_ticket(self, vmid): + """VNC 접속 티쌓 생성""" + + def get_vm_status(self, vmid): + """VM 상태 확읞""" +``` + +#### 1.2 데읎터베읎슀 몚덞 확장 +```python +# models/farmq_models.py +class ProxmoxVM(FarmqBase): + __tablename__ = 'proxmox_vms' + + id = Column(Integer, primary_key=True) + pharmacy_id = Column(Integer, ForeignKey('pharmacy_info.id')) + proxmox_host_ip = Column(String(15)) # 100.64.0.x + vmid = Column(Integer) + vm_name = Column(String(100)) + vm_type = Column(String(50)) # windows, linux + status = Column(String(20)) # running, stopped + cpu_cores = Column(Integer) + memory_mb = Column(Integer) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) +``` + +### Phase 2: VNC 웹 큎띌읎얞튞 구현 (2-3음) + +#### 2.1 noVNC 통합 (권장) +```html + + + + + {{ vm_name }} - VNC Console + + + +
+ +
+ + + + +``` + +#### 2.2 WebSocket Proxy 서버 +```python +# vnc_proxy.py +import websockets +import asyncio + +class VNCProxy: + async def proxy_vnc_connection(self, websocket, path): + # Flask → Proxmox VNC 연결 쀑계 + session_id = extract_session_id(path) + proxmox_vnc = await connect_to_proxmox_vnc(session_id) + + # 양방향 데읎터 쀑계 + await asyncio.gather( + relay_websocket_to_vnc(websocket, proxmox_vnc), + relay_vnc_to_websocket(proxmox_vnc, websocket) + ) +``` + +### Phase 3: Flask Admin 통합 (1음) + +#### 3.1 VNC 연결 API 엔드포읞튞 +```python +# app.py +@app.route('/api/vm//vnc', methods=['POST']) +def connect_vm_vnc(vm_id): + """VM VNC 연결 섞션 생성""" + try: + vm = get_vm_by_id(vm_id) + proxmox = ProxmoxClient(vm.proxmox_host_ip, username, password) + + # VNC 티쌓 생성 + vnc_ticket = proxmox.get_vnc_ticket(vm.vmid) + + # 섞션 생성 + session_id = create_vnc_session(vm_id, vnc_ticket) + + return jsonify({ + 'session_id': session_id, + 'vnc_url': f'/vnc/console/{session_id}', + 'vm_info': vm.to_dict() + }) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/vnc/console/') +def vnc_console(session_id): + """VNC 윘솔 페읎지""" + session = get_vnc_session(session_id) + return render_template('vnc/novnc.html', + session=session, + vm_name=session['vm_name']) +``` + +#### 3.2 앜국 ꎀ늬 페읎지 업데읎튞 +```html + +
+
+
가상 ëšžì‹  목록
+
+
+ + + + + + + + + + + + {% for vm in pharmacy_vms %} + + + + + + + + {% endfor %} + +
VM 읎늄상태타입늬소슀액션
{{ vm.vm_name }} + {% if vm.status == 'running' %} + 싀행 쀑 + {% else %} + 정지 + {% endif %} + {{ vm.vm_type }}{{ vm.cpu_cores }}C / {{ vm.memory_mb }}MB + {% if vm.status == 'running' %} + + {% endif %} + +
+
+
+ + +``` + +## 📊 데읎터 흐멄 + +### 1. VM 목록 동Ʞ화 +``` +Proxmox API → Flask Backend → Database → Admin Dashboard +``` + +### 2. VNC 연결 프로섞슀 +``` +1. 사용자가 "VNC 접속" 버튌 큎늭 +2. Flask API가 Proxmox API 혞출하여 VNC 티쌓 생성 +3. WebSocket 프록시 섞션 생성 +4. 새 람띌우저 탭에서 noVNC 큎띌읎얞튞 싀행 +5. 싀시간 VM 화멎 표시 +``` + +## 🔐 볎안 고렀사항 + +### 읞슝 및 권한 +```python +# 앜국별 VM ì ‘ê·Œ 권한 검슝 +def check_vm_access_permission(user_id, vm_id): + """사용자가 핎당 VM에 ì ‘ê·Œ 권한읎 있는지 확읞""" + user_pharmacy = get_user_pharmacy(user_id) + vm_pharmacy = get_vm_pharmacy(vm_id) + return user_pharmacy.id == vm_pharmacy.id + +# VNC 섞션 시간 제한 +VNC_SESSION_TIMEOUT = 3600 # 1시간 +``` + +### 넀튞워크 볎안 +- **Headscale 넀튞워크 낎부**에서만 Proxmox ì ‘ê·Œ +- **HTTPS/WSS** 암혞화 통신 +- **섞션 êž°ë°˜** 음회성 VNC 토큰 + +## 🎚 UI/UX 섀계 + +### 메읞 대시볎드 +``` +┌─────────────────────────────────────────┐ +│ 📊 팜큐 ꎀ늬 대시볎드 │ +├────────────────────────────────────────── +│ 앜국: 섞종옚누늬앜국 │ +│ ┌─────────┬──────────┬────────┬─────────┐ │ +│ │ VM 읎늄 │ 상태 │ 타입 │ 액션 │ │ +│ ├─────────┌──────────┌────────┌────────── │ +│ │ POS-01 │ 🟢 싀행쀑 │ Win11 │ [VNC접속]│ │ +│ │ SERVER │ 🟢 싀행쀑 │ Ubuntu │ [VNC접속]│ │ +│ │ BACKUP │ ⚪ 정지 │ Win10 │ [시작] │ │ +│ └─────────┮──────────┮────────┮─────────┘ │ +└─────────────────────────────────────────┘ +``` + +### VNC 윘솔 화멎 +``` +┌─────────────────────────────────────────┐ +│ 🖥 POS-01 (Windows 11) - VNC Console │ +├────────────────────────────────────────── +│ [전첎화멎] [큎늜볎드] [Ctrl+Alt+Del] │ +├────────────────────────────────────────── +│ │ +│ VM 화멎읎 여Ʞ에 표시 │ +│ (noVNC Canvas) │ +│ │ +└─────────────────────────────────────────┘ +``` + +## 📋 구현 첎크늬슀튞 + +### Backend (Flask) +- [ ] Proxmox API 큎띌읎얞튞 구현 +- [ ] ProxmoxVM 데읎터 몚덞 생성 +- [ ] VNC 섞션 ꎀ늬 시슀템 +- [ ] WebSocket 프록시 서버 +- [ ] API 엔드포읞튞 구현 + +### Frontend (Templates) +- [ ] noVNC 띌읎람러늬 통합 +- [ ] VNC 윘솔 페읎지 템플늿 +- [ ] 앜국 상섞 페읎지 VM 섹션 +- [ ] JavaScript VNC 연결 핚수 + +### 시슀템 통합 +- [ ] VM 목록 자동 동Ʞ화 +- [ ] 권한 검슝 시슀템 +- [ ] 에러 처늬 및 로깅 +- [ ] 성능 최적화 + +## 🚀 배포 계획 + +### 개발 환겜 테슀튞 +1. **로컬 Proxmox 테슀튞**: VirtualBox/VMware로 Proxmox VE 섀치 +2. **noVNC 연동 테슀튞**: Ʞ볞 VNC 연결 확읞 +3. **Headscale 넀튞워크 테슀튞**: 원격 Proxmox ì ‘ê·Œ + +### 욎영 환겜 적용 +1. **점진적 배포**: 1개 앜국부터 테슀튞 +2. **몚니터링 시슀템**: VNC 연결 로귞 및 성능 잡정 +3. **백업 ì ‘ê·Œ 방법**: VNC 싀팚 시 SSH/RDP 대안 + +## 💡 추가 Ʞ능 아읎디얎 + +### Phase 2 고꞉ Ʞ능 +- **닀쀑 몚니터 지원**: VM읎 여러 화멎을 사용하는 겜우 +- **큎늜볎드 공유**: 로컬 PC ↔ 원격 VM 텍슀튞 복사 +- **파음 전송**: 드래귞앀드롭 파음 업로드 +- **슀크늰샷 캡처**: 묞제 핎결을 위한 화멎 저장 +- **섞션 녹화**: 작업 곌정 Ʞ록 + +### 몚니터링 및 분석 +- **VNC 사용 통계**: 접속 시간, 빈도 분석 +- **VM 성능 몚니터링**: CPU, 메몚늬 사용률 싀시간 표시 +- **접속 읎력 ꎀ늬**: ì–žì œ, 누가, ì–Žë–€ VM에 접속했는지 로귞 + +## 🎯 예상 횚곌 + +### 업묎 횚윚성 +- **슉시 원격 지원**: 앜국 직원 도움 요청 시 바로 화멎 접속 +- **쀑앙 집쀑 ꎀ늬**: 몚든 앜국 VM을 한 곳에서 ꎀ늬 +- **묞제 핎결 시간 닚축**: 전화 섀명 → 직접 화멎 제얎 + +### Ʞ술적 장점 +- **Headscale 넀튞워크 활용**: Ʞ졎 읞프띌 최대한 활용 +- **람띌우저 êž°ë°˜**: 별도 소프튞웚얎 섀치 불필요 +- **확장성**: 새로욎 앜국 추가 시 자동 연동 + +--- + +## 📞 묞의 및 지원 + +읎 시슀템 구현 곌정에서 Ʞ술적 읎슈나 추가 요구사항읎 있윌멎 얞제든 묞의하섞요. + +**핵심 목표**: 🖱 **원큎늭** → 🖥 **VM 화멎 접속** \ No newline at end of file diff --git a/PharmQ-SaaS-Service-Plan.md b/PharmQ-SaaS-Service-Plan.md new file mode 100644 index 0000000..b789b99 --- /dev/null +++ b/PharmQ-SaaS-Service-Plan.md @@ -0,0 +1,297 @@ +# PharmQ SaaS 구독 서비슀 ꎀ늬 시슀템 Ʞ획서 + +## 1. 프로젝튞 개요 + +### 1.1 목적 +- PharmQ가 제공하는 닀양한 서비슀에 대한 앜국별 구독 ꎀ늬 +- SaaS 형태의 곌ꞈ 서비슀 êž°ë°˜ 마렚 +- 앜국별 서비슀 읎용 현황 싀시간 몚니터링 + +### 1.2 서비슀 띌읞업 +1. **큎띌우드 PC** (Proxmox êž°ë°˜ 가상 데슀크톱) +2. **AI CCTV** (읞공지능 êž°ë°˜ 볎안 몚니터링) +3. **CRM** (고객 ꎀ계 ꎀ늬 시슀템) + +## 2. 데읎터베읎슀 슀킀마 섀계 + +### 2.1 새로 추가할 테읎랔 + +#### 2.1.1 `service_products` - 서비슀 상품 마슀터 +```sql +CREATE TABLE service_products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_code VARCHAR(20) UNIQUE NOT NULL, -- 'CLOUD_PC', 'AI_CCTV', 'CRM' + product_name VARCHAR(100) NOT NULL, -- '큎띌우드 PC', 'AI CCTV', 'CRM' + description TEXT, -- 서비슀 상섞 섀명 + monthly_price DECIMAL(10,2) NOT NULL, -- 월 구독료 + setup_fee DECIMAL(10,2) DEFAULT 0, -- 쎈Ʞ 섀치비 + is_active BOOLEAN DEFAULT TRUE, -- 서비슀 활성화 여부 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +#### 2.1.2 `pharmacy_subscriptions` - 앜국별 구독 현황 +```sql +CREATE TABLE pharmacy_subscriptions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pharmacy_id INTEGER NOT NULL, -- pharmacy_info.id ì°žì¡° + product_id INTEGER NOT NULL, -- service_products.id ì°žì¡° + subscription_status VARCHAR(20) NOT NULL, -- 'ACTIVE', 'SUSPENDED', 'CANCELLED' + start_date DATE NOT NULL, -- 구독 시작음 + end_date DATE, -- 구독 종료음 (NULL읎멎 묎제한) + next_billing_date DATE, -- 닀음 결제음 + monthly_fee DECIMAL(10,2) NOT NULL, -- 싀제 적용 월 구독료 + notes TEXT, -- 특읎사항 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (pharmacy_id) REFERENCES pharmacy_info(id), + FOREIGN KEY (product_id) REFERENCES service_products(id), + UNIQUE(pharmacy_id, product_id) -- 앜국-상품당 하나의 구독만 +); +``` + +#### 2.1.3 `subscription_usage_logs` - 서비슀 읎용 로귞 +```sql +CREATE TABLE subscription_usage_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + subscription_id INTEGER NOT NULL, -- pharmacy_subscriptions.id ì°žì¡° + usage_type VARCHAR(50) NOT NULL, -- 'LOGIN', 'API_CALL', 'STORAGE_USE' 등 + usage_amount INTEGER DEFAULT 1, -- 사용량 (로귞읞 횟수, API 혞출 수 등) + usage_date DATE NOT NULL, -- 사용음 + metadata JSON, -- 추가 메타데읎터 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (subscription_id) REFERENCES pharmacy_subscriptions(id) +); +``` + +#### 2.1.4 `billing_history` - 결제 읎력 +```sql +CREATE TABLE billing_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + subscription_id INTEGER NOT NULL, -- pharmacy_subscriptions.id ì°žì¡° + billing_period_start DATE NOT NULL, -- 곌ꞈ êž°ê°„ 시작 + billing_period_end DATE NOT NULL, -- 곌ꞈ êž°ê°„ 종료 + amount DECIMAL(10,2) NOT NULL, -- 청구 ꞈ액 + billing_status VARCHAR(20) NOT NULL, -- 'PENDING', 'PAID', 'OVERDUE', 'CANCELLED' + billing_date DATE, -- 싀제 결제음 + payment_method VARCHAR(50), -- 결제 수닚 + invoice_number VARCHAR(100), -- 청구서 번혞 + notes TEXT, -- 결제 ꎀ렚 메몚 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (subscription_id) REFERENCES pharmacy_subscriptions(id) +); +``` + +### 2.2 Ʞ졎 테읎랔 연동 +- `pharmacy_info` 테읎랔곌 `pharmacy_subscriptions` 테읎랔을 연결 +- 사용자 ꎀ늬에서 앜국별 구독 서비슀 현황 표시 + +## 3. 프론튞엔드 UI/UX 섀계 + +### 3.1 대시볎드 개선 + +#### 3.1.1 메읞 대시볎드 (`/`) +- **구독 서비슀 현황 칎드** 추가 + ``` + ┌─────────────────────────────────────────┐ + │ 📊 구독 서비슀 현황 │ + ├────────────────────────────────────────── + │ 큎띌우드 PC │ 12/14 앜국 (85.7%) │ + │ AI CCTV │ 8/14 앜국 (57.1%) │ + │ CRM │ 10/14 앜국 (71.4%) │ + ├────────────────────────────────────────── + │ 쎝 월 맀출 │ ₩2,450,000 │ + └─────────────────────────────────────────┘ + ``` + +#### 3.1.2 서비슀별 상태 읞디쌀읎터 +- 각 서비슀별 색상 윔드 적용 + - 🟢 큎띌우드 PC (녹색) + - 🔵 AI CCTV (파란색) + - 🟡 CRM (녞란색) + +### 3.2 앜국 ꎀ늬 페읎지 개선 (`/pharmacy`) + +#### 3.2.1 앜국 목록에 구독 상태 표시 +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 앜국명 │ 위치 │ 구독 서비슀 │ 월 구독료 │ 액션 │ +├────────────────────────────────────────────────────────────────── +│ 서욞앜국 │ 서욞 강낚구 │ 🟢 💻 🔵 📷 🟡 📊 │ ₩180,000 │ ⚙ │ +│ 부산앜국 │ 부산 핎욎대 │ 🟢 💻 🔵 📷 │ ₩120,000 │ ⚙ │ +│ 대구앜국 │ 대구 쀑구 │ 🟢 💻 │ ₩60,000 │ ⚙ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +#### 3.2.2 앜국 상섞 페읎지 구독 탭 추가 +- Ʞ졎: Ʞ볞정볎, 넀튞워크정볎, 연결된 ëšžì‹  +- **신규**: **구독 서비슀** 탭 추가 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ [Ʞ볞정볎] [넀튞워크정볎] [연결된 ëšžì‹ ] [구독 서비슀] ←← 신규 탭 │ +├────────────────────────────────────────────────────────────────── +│ 📊 구독 쀑읞 서비슀 │ +│ │ +│ 🟢 큎띌우드 PC ₩60,000/월 [ꎀ늬] │ +│ ├─ 구독Ʞ간: 2024.01.15 ~ 묎제한 │ +│ ├─ 닀음결제: 2025.10.15 │ +│ └─ 상태: 정상 (ACTIVE) │ +│ │ +│ 🔵 AI CCTV ₩80,000/월 [ꎀ늬] │ +│ ├─ 구독Ʞ간: 2024.03.01 ~ 묎제한 │ +│ ├─ 닀음결제: 2025.10.01 │ +│ └─ 상태: 정상 (ACTIVE) │ +│ │ +│ 📊 구독 가능한 서비슀 │ +│ │ +│ 🟡 CRM 시슀템 ₩40,000/월 [가입] │ +│ └─ 고객 ꎀ계 ꎀ늬 및 맀출 분석 도구 │ +│ │ +│ [+ 새 서비슀 구독하Ʞ] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 3.3 새로욎 메뉎 추가 + +#### 3.3.1 사읎드바 메뉎 구조 개선 +``` +PharmQ Super Admin (PSA) +├── 📊 대시볎드 +├── 🏥 앜국 ꎀ늬 +├── 👥 PQON 사용자 ꎀ늬 +├── 💻 ëšžì‹  ꎀ늬 +├── 🖥 VM ꎀ늬 (VNC) +├── 📊 구독 서비슀 ꎀ늬 ←← 신규 메뉎 +│ ├── 구독 현황 조회 +│ ├── 서비슀 상품 ꎀ늬 +│ ├── 결제 읎력 조회 +│ └── 사용량 통계 +├── 📈 맀출 대시볎드 ←← 신규 메뉎 +└── 🔗 Medivault +``` + +#### 3.3.2 구독 서비슀 ꎀ늬 페읎지 (`/subscriptions`) +- **전첎 구독 현황 테읎랔** +- **서비슀별 필터링** +- **구독 상태별 필터링** (활성/음시정지/핎지) +- **월별 맀출 찚튞** + +#### 3.3.3 맀출 대시볎드 (`/revenue`) +- **월별 맀출 튾렌드** +- **서비슀별 맀출 비쀑** +- **앜국별 구독료 순위** +- **신규 구독/핎지 통계** + +## 4. API 섀계 + +### 4.1 구독 ꎀ늬 API + +#### 4.1.1 구독 현황 조회 +``` +GET /api/subscriptions +GET /api/subscriptions/pharmacy/{pharmacy_id} +GET /api/subscriptions/product/{product_code} +``` + +#### 4.1.2 구독 생성/수정 +``` +POST /api/subscriptions # 새 구독 생성 +PUT /api/subscriptions/{subscription_id} # 구독 정볎 수정 +DELETE /api/subscriptions/{subscription_id} # 구독 핎지 +``` + +#### 4.1.3 서비슀 상품 ꎀ늬 +``` +GET /api/products # 상품 목록 +POST /api/products # 상품 등록 +PUT /api/products/{product_id} # 상품 수정 +``` + +### 4.2 결제 및 통계 API + +#### 4.2.1 결제 ꎀ렚 +``` +GET /api/billing/history/{subscription_id} # 결제 읎력 +POST /api/billing/invoice # 청구서 생성 +PUT /api/billing/{billing_id}/status # 결제 상태 업데읎튞 +``` + +#### 4.2.2 통계 및 늬포튞 +``` +GET /api/analytics/revenue/monthly # 월별 맀출 +GET /api/analytics/subscriptions/summary # 구독 요앜 +GET /api/analytics/usage/{subscription_id} # 서비슀 사용량 +``` + +## 5. 구현 닚계별 로드맵 + +### Phase 1: Ʞ볞 구조 구축 (1-2죌) +- [x] 데읎터베읎슀 슀킀마 생성 +- [ ] Ʞ볞 API 엔드포읞튞 구현 +- [ ] 대시볎드 구독 현황 칎드 추가 + +### Phase 2: 앜국별 구독 ꎀ늬 (2-3죌) +- [ ] 앜국 상섞 페읎지 구독 탭 추가 +- [ ] 구독 생성/수정/핎지 Ʞ능 +- [ ] 앜국 목록 구독 상태 표시 + +### Phase 3: 서비슀 ꎀ늬 및 통계 (3-4죌) +- [ ] 구독 서비슀 ꎀ늬 메뉎 구현 +- [ ] 맀출 대시볎드 구현 +- [ ] 사용량 로깅 시슀템 + +### Phase 4: 곌ꞈ 시슀템 (4-6죌) +- [ ] 자동 결제 시슀템 +- [ ] 청구서 생성 +- [ ] 결제 연동 (포튞원/토슀페읎뚌잠 등) + +## 6. Ʞ술 슀택 + +### 6.1 백엔드 +- **Database**: SQLite (개발) → PostgreSQL (욎영) +- **API**: Flask REST API +- **결제**: 포튞원(PortOne) 또는 토슀페읎뚌잠 + +### 6.2 프론튞엔드 +- **Framework**: Bootstrap 5 + Jinja2 +- **Charts**: Chart.js 또는 D3.js +- **Icons**: Font Awesome + +### 6.3 몚니터링 +- **사용량 추적**: Custom logging system +- **알늌**: 결제 싀팚, 구독 만료 등 + +## 7. 볎안 및 컎플띌읎얞슀 + +### 7.1 데읎터 볎안 +- 결제 정볎 암혞화 +- 개읞정볎 볎혞법 쀀수 +- API 읞슝/읞가 첎계 + +### 7.2 백업 및 복구 +- 데읎터베읎슀 음음 백업 +- 결제 데읎터 별도 볎ꎀ +- 장애 복구 프로섞슀 + +--- + +## 8. 예상 횚곌 + +### 8.1 비슈니슀 횚곌 +- **맀출 가시화**: 싀시간 구독 맀출 현황 파악 +- **고객 ꎀ늬**: 앜국별 서비슀 읎용 팹턮 분석 +- **확장성**: 새로욎 서비슀 추가 용읎 + +### 8.2 욎영 횚윚화 +- **자동화**: 구독/핎지/결제 프로섞슀 자동화 +- **몚니터링**: 서비슀별 읎용 현황 싀시간 추적 +- **늬포팅**: 월별/분Ʞ별 맀출 늬포튞 자동 생성 + +--- + +**작성음**: 2025년 9월 11음 +**작성자**: PharmQ Development Team +**버전**: v1.0 \ No newline at end of file diff --git a/add-client.sh b/add-client.sh new file mode 100755 index 0000000..4f5ef0b --- /dev/null +++ b/add-client.sh @@ -0,0 +1,196 @@ +#!/bin/bash + +# ============================================================================= +# FARMQ Headscale 큎띌읎얞튞 자동 등록 슀크늜튞 +# ============================================================================= +# 사용법: ./add-client.sh [사용자명] [뚞신명] +# 예시: ./add-client.sh pharmacy-01 busan-store-pc +# ============================================================================= + +set -e # 였류 발생 시 슀크늜튞 쀑닚 + +# 색상 정의 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 로고 +echo -e "${BLUE}" +echo " ███████╗ █████╗ ██████╗ ██╗ ██╗ ██████╗ " +echo " ██╔════╝██╔══██╗██╔══██╗████╗ ████║██╔═══██╗" +echo " █████╗ ███████║██████╔╝██╔████╔██║██║ ██║" +echo " ██╔══╝ ██╔══██║██╔══██╗██║╚██╔╝██║██║▄▄ ██║" +echo " ██║ ██║ ██║██║ ██║██║ ╚═╝ ██║╚██████╔╝" +echo " ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══▀▀═╝ " +echo -e "${NC}" +echo -e "${GREEN}🏥 FARMQ Headscale 큎띌읎얞튞 자동 등록 슀크늜튞${NC}" +echo "============================================================" + +# 섀정 변수 +HEADSCALE_SERVER="192.168.0.151:8070" +HEADSCALE_URL="http://${HEADSCALE_SERVER}" +FARMQ_ADMIN_URL="http://192.168.0.151:5001" + +# 파띌믞터 확읞 +USER_NAME=${1:-""} +MACHINE_NAME=${2:-""} + +# 사용자 입력 받Ʞ +if [[ -z "$USER_NAME" ]]; then + echo -e "${YELLOW}📝 사용자명을 입력하섞요 (예: pharmacy-01, store-busan):${NC}" + read -p "사용자명: " USER_NAME +fi + +if [[ -z "$MACHINE_NAME" ]]; then + echo -e "${YELLOW}📝 뚞신명을 입력하섞요 (예: pos-terminal, office-pc):${NC}" + read -p "뚞신명: " MACHINE_NAME +fi + +# 입력값 검슝 +if [[ -z "$USER_NAME" ]] || [[ -z "$MACHINE_NAME" ]]; then + echo -e "${RED}❌ 사용자명곌 뚞신명은 필수입니닀.${NC}" + exit 1 +fi + +echo -e "${BLUE}📋 섀정 정볎:${NC}" +echo " 사용자명: $USER_NAME" +echo " 뚞신명: $MACHINE_NAME" +echo " Headscale 서버: $HEADSCALE_SERVER" +echo "" + +# 확읞 메시지 +echo -e "${YELLOW}⚠ 읎 섀정윌로 진행하시겠습니까? (y/N)${NC}" +read -p "진행: " CONFIRM +if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then + echo -e "${RED}❌ 췚소되었습니닀.${NC}" + exit 1 +fi + +echo -e "${GREEN}🚀 큎띌읎얞튞 등록을 시작합니닀...${NC}" + +# 1. 시슀템 업데읎튞 +echo -e "${BLUE}📊 시슀템 업데읎튞 쀑...${NC}" +sudo apt update -qq + +# 2. Tailscale 섀치 확읞 +if ! command -v tailscale &> /dev/null; then + echo -e "${YELLOW}📥 Tailscale 섀치 쀑...${NC}" + curl -fsSL https://tailscale.com/install.sh | sh +else + echo -e "${GREEN}✅ Tailscale읎 읎믞 섀치되얎 있습니닀.${NC}" +fi + +# 3. Ʞ졎 Tailscale 연결 핎제 (있는 겜우) +echo -e "${BLUE}🧹 Ʞ졎 연결 정늬 쀑...${NC}" +sudo tailscale logout 2>/dev/null || true +sudo tailscale down 2>/dev/null || true + +# 4. Headscale 서버 연결 테슀튞 +echo -e "${BLUE}🔍 Headscale 서버 연결 테슀튞 쀑...${NC}" +if ! curl -s --connect-timeout 5 "$HEADSCALE_URL/health" > /dev/null 2>&1; then + echo -e "${RED}❌ Headscale 서버에 연결할 수 없습니닀: $HEADSCALE_URL${NC}" + echo -e "${YELLOW}💡 서버가 싀행 쀑읎고 방화벜 섀정을 확읞하섞요.${NC}" + exit 1 +fi +echo -e "${GREEN}✅ Headscale 서버 연결 성공${NC}" + +# 5. Pre-auth key 생성 (서버에서 싀행) +echo -e "${BLUE}🔑 Pre-auth key 생성 쀑...${NC}" +echo -e "${YELLOW}📝 Headscale 서버에서 닀음 명령얎륌 싀행핎알 합니닀:${NC}" +echo "" +echo -e "${GREEN}# SSH로 서버 접속 후 싀행:${NC}" +echo "cd /srv/headscale-setup" +echo "docker exec headscale headscale users create $USER_NAME 2>/dev/null || true" +echo "docker exec headscale headscale preauthkeys create --user $USER_NAME --expiration 1h" +echo "" + +# Pre-auth key 입력 받Ʞ +echo -e "${YELLOW}🔐 생성된 Pre-auth key륌 입력하섞요:${NC}" +read -p "Pre-auth key: " PREAUTH_KEY + +if [[ -z "$PREAUTH_KEY" ]]; then + echo -e "${RED}❌ Pre-auth key는 필수입니닀.${NC}" + exit 1 +fi + +# 6. Tailscale을 Headscale 서버에 연결 +echo -e "${BLUE}🔗 Headscale 서버에 연결 쀑...${NC}" + +# 뚞신명 섀정 +sudo tailscale up \ + --login-server="$HEADSCALE_URL" \ + --authkey="$PREAUTH_KEY" \ + --hostname="$MACHINE_NAME" \ + --accept-dns=false \ + --reset + +# 7. 연결 상태 확읞 +echo -e "${BLUE}🔍 연결 상태 확읞 쀑...${NC}" +sleep 5 + +# Tailscale 상태 확읞 +if tailscale status >/dev/null 2>&1; then + echo -e "${GREEN}✅ Tailscale 서비슀 정상 작동${NC}" + + # IP 죌소 확읞 + TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "IP 조회 싀팚") + echo -e "${GREEN}📍 할당된 IP 죌소: $TAILSCALE_IP${NC}" + + # 연결된 녾드 목록 + echo -e "${BLUE}🌐 연결된 녾드 목록:${NC}" + tailscale status + +else + echo -e "${RED}❌ Tailscale 연결에 싀팚했습니닀.${NC}" + echo -e "${YELLOW}💡 로귞 확읞: sudo journalctl -u tailscaled -f${NC}" + exit 1 +fi + +# 8. 넀튞워크 연결 테슀튞 +echo -e "${BLUE}🧪 넀튞워크 연결 테슀튞 쀑...${NC}" + +# 서버와의 연결 테슀튞 +if ping -c 3 100.64.0.1 >/dev/null 2>&1; then + echo -e "${GREEN}✅ Headscale 서버와 통신 성공 (100.64.0.1)${NC}" +else + echo -e "${YELLOW}⚠ 서버와의 직접 통신은 싀팚했지만 정상음 수 있습니닀.${NC}" +fi + +# 9. 시슀템 서비슀 활성화 +echo -e "${BLUE}⚙ 시슀템 서비슀 섀정 쀑...${NC}" +sudo systemctl enable tailscaled +sudo systemctl start tailscaled + +# 10. 방화벜 섀정 (선택사항) +if command -v ufw &> /dev/null; then + echo -e "${BLUE}🔥 방화벜 섀정 쀑...${NC}" + sudo ufw allow in on tailscale0 2>/dev/null || true +fi + +# 11. 완료 메시지 +echo -e "${GREEN}" +echo "🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉" +echo " FARMQ 큎띌읎얞튞 등록 완료!" +echo "🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉" +echo -e "${NC}" +echo -e "${GREEN}✅ 큎띌읎얞튞가 성공적윌로 등록되었습니닀!${NC}" +echo "" +echo -e "${BLUE}📊 연결 정볎:${NC}" +echo " • 사용자명: $USER_NAME" +echo " • 뚞신명: $MACHINE_NAME" +echo " • Tailscale IP: $TAILSCALE_IP" +echo " • 서버 죌소: $HEADSCALE_SERVER" +echo "" +echo -e "${BLUE}🌐 ꎀ늬 페읎지:${NC}" +echo " • FARMQ ꎀ늬자 페읎지: $FARMQ_ADMIN_URL" +echo " • Headplane UI: http://192.168.0.151:3000" +echo "" +echo -e "${BLUE}🔧 유용한 명령얎:${NC}" +echo " • 상태 확읞: tailscale status" +echo " • IP 죌소 확읞: tailscale ip" +echo " • 로귞 확읞: sudo journalctl -u tailscaled -f" +echo " • 재시작: sudo systemctl restart tailscaled" +echo "" +echo -e "${GREEN}🎊 읎제 FARMQ 넀튞워크의 음부가 되었습니닀!${NC}" \ No newline at end of file diff --git a/clean-database.py b/clean-database.py new file mode 100644 index 0000000..87ac88a --- /dev/null +++ b/clean-database.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +데읎터베읎슀 정늬 - 묞제가 되는 테읎랔듀 제거 +""" + +import sqlite3 +import os +from datetime import datetime + +def clean_database(): + """묞제가 되는 테읎랔듀 제거""" + db_path = '/srv/headscale-setup/data/db.sqlite' + backup_path = f'/srv/headscale-setup/data/db.sqlite.clean_backup.{datetime.now().strftime("%Y%m%d_%H%M%S")}' + + print("🧹 데읎터베읎슀 정늬 - 묞제 테읎랔 제거") + print("=" * 50) + + # 백업 생성 + print(f"📊 백업 생성: {backup_path}") + import shutil + shutil.copy2(db_path, backup_path) + + # 데읎터베읎슀 연결 + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + try: + # 왞래킀 제앜조걎 비활성화 + cursor.execute("PRAGMA foreign_keys = OFF") + + # 묞제가 되는 테읎랔듀 확읞 + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%pharmacy%'") + problem_tables = cursor.fetchall() + print(f"🎯 제거할 테읎랔듀: {[table[0] for table in problem_tables]}") + + # 테읎랔듀 제거 + for table in problem_tables: + table_name = table[0] + print(f"🗑 테읎랔 제거: {table_name}") + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + + # monitoring_data, machine_specs 등도 제거 (필요시) + additional_tables = ['monitoring_data', 'machine_specs'] + for table_name in additional_tables: + cursor.execute(f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}'") + if cursor.fetchone(): + print(f"🗑 추가 테읎랔 제거: {table_name}") + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + + # 변겜사항 컀밋 + conn.commit() + + # 낚은 테읎랔 확읞 + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") + remaining_tables = cursor.fetchall() + print(f"✅ 낚은 테읎랔듀: {[table[0] for table in remaining_tables]}") + + # 묎결성 검사 + cursor.execute("PRAGMA integrity_check") + integrity = cursor.fetchone()[0] + print(f"🔍 묎결성 검사: {integrity}") + + print("✅ 데읎터베읎슀 정늬 완료!") + print(f"📊 백업 위치: {backup_path}") + + except Exception as e: + print(f"❌ 였류: {e}") + conn.rollback() + raise + finally: + conn.close() + +if __name__ == '__main__': + clean_database() \ No newline at end of file diff --git a/farmq-admin/app.py b/farmq-admin/app.py index 683697f..b580dc0 100644 --- a/farmq-admin/app.py +++ b/farmq-admin/app.py @@ -825,6 +825,328 @@ def create_app(config_name=None): 'error': f'서버 였류: {str(e)}' }), 500 + # =================== 구독 서비슀 ꎀ늬 API =================== + + @app.route('/api/subscriptions/stats', methods=['GET']) + def api_subscription_stats(): + """대시볎드용 구독 현황 통계""" + try: + import sqlite3 + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 서비슀별 구독 현황 + cursor.execute(''' + SELECT + sp.product_code, + sp.product_name, + COUNT(ps.id) as subscription_count, + SUM(ps.monthly_fee) as total_monthly_revenue + FROM service_products sp + LEFT JOIN pharmacy_subscriptions ps ON sp.id = ps.product_id + AND ps.subscription_status = 'ACTIVE' + GROUP BY sp.id, sp.product_code, sp.product_name + ORDER BY sp.product_code + ''') + + services = [] + total_revenue = 0 + total_subscriptions = 0 + + for row in cursor.fetchall(): + product_code, product_name, count, revenue = row + revenue = revenue or 0 + services.append({ + 'code': product_code, + 'name': product_name, + 'count': count, + 'revenue': revenue + }) + total_revenue += revenue + total_subscriptions += count + + # 전첎 앜국 수 + cursor.execute("SELECT COUNT(*) FROM pharmacies") + total_pharmacies = cursor.fetchone()[0] + + # 구독 쀑읞 앜국 수 + cursor.execute(''' + SELECT COUNT(DISTINCT pharmacy_id) + FROM pharmacy_subscriptions + WHERE subscription_status = 'ACTIVE' + ''') + subscribed_pharmacies = cursor.fetchone()[0] + + conn.close() + + return jsonify({ + 'success': True, + 'data': { + 'services': services, + 'total_revenue': total_revenue, + 'total_subscriptions': total_subscriptions, + 'total_pharmacies': total_pharmacies, + 'subscribed_pharmacies': subscribed_pharmacies, + 'subscription_rate': round(subscribed_pharmacies / total_pharmacies * 100, 1) if total_pharmacies > 0 else 0 + } + }) + + except Exception as e: + print(f"❌ 구독 통계 조회 였류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/pharmacies/subscriptions', methods=['GET']) + def api_pharmacy_subscriptions(): + """앜국별 구독 현황 조회""" + try: + import sqlite3 + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 앜국별 구독 현황 조회 + cursor.execute(''' + SELECT + p.id, + p.pharmacy_name, + p.manager_name, + p.address, + GROUP_CONCAT(sp.product_code) as subscribed_services, + SUM(ps.monthly_fee) as total_monthly_fee + FROM pharmacies p + LEFT JOIN pharmacy_subscriptions ps ON p.id = ps.pharmacy_id + AND ps.subscription_status = 'ACTIVE' + LEFT JOIN service_products sp ON ps.product_id = sp.id + GROUP BY p.id, p.pharmacy_name, p.manager_name, p.address + ORDER BY total_monthly_fee DESC NULLS LAST, p.pharmacy_name + ''') + + pharmacies = [] + for row in cursor.fetchall(): + pharmacy_id, name, manager, address, services, fee = row + + # 구독 서비슀 늬슀튞 변환 + service_list = [] + if services: + for service_code in services.split(','): + service_list.append(service_code) + + pharmacies.append({ + 'id': pharmacy_id, + 'name': name, + 'manager': manager, + 'address': address, + 'subscribed_services': service_list, + 'monthly_fee': fee or 0 + }) + + conn.close() + + return jsonify({ + 'success': True, + 'data': pharmacies + }) + + except Exception as e: + print(f"❌ 앜국 구독 현황 조회 였류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/pharmacy//subscriptions', methods=['GET']) + def api_pharmacy_subscription_detail(pharmacy_id): + """특정 앜국의 구독 상섞 현황""" + try: + import sqlite3 + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 앜국 Ʞ볞 정볎 + cursor.execute("SELECT pharmacy_name FROM pharmacies WHERE id = ?", (pharmacy_id,)) + pharmacy_result = cursor.fetchone() + if not pharmacy_result: + return jsonify({'success': False, 'error': '앜국을 찟을 수 없습니닀.'}), 404 + + # 구독 쀑읞 서비슀 + cursor.execute(''' + SELECT + ps.id, + sp.product_code, + sp.product_name, + ps.monthly_fee, + ps.start_date, + ps.next_billing_date, + ps.subscription_status, + ps.notes + FROM pharmacy_subscriptions ps + JOIN service_products sp ON ps.product_id = sp.id + WHERE ps.pharmacy_id = ? AND ps.subscription_status = 'ACTIVE' + ORDER BY sp.product_name + ''', (pharmacy_id,)) + + active_subscriptions = [] + for row in cursor.fetchall(): + sub_id, code, name, fee, start, next_bill, status, notes = row + active_subscriptions.append({ + 'id': sub_id, + 'code': code, + 'name': name, + 'monthly_fee': fee, + 'start_date': start, + 'next_billing_date': next_bill, + 'status': status, + 'notes': notes + }) + + # 구독 가능한 서비슀 (믞구독 서비슀) + cursor.execute(''' + SELECT sp.id, sp.product_code, sp.product_name, sp.monthly_price, sp.description + FROM service_products sp + WHERE sp.is_active = 1 + AND sp.id NOT IN ( + SELECT ps.product_id + FROM pharmacy_subscriptions ps + WHERE ps.pharmacy_id = ? AND ps.subscription_status = 'ACTIVE' + ) + ORDER BY sp.product_name + ''', (pharmacy_id,)) + + available_services = [] + for row in cursor.fetchall(): + prod_id, code, name, price, desc = row + available_services.append({ + 'id': prod_id, + 'code': code, + 'name': name, + 'monthly_price': price, + 'description': desc + }) + + conn.close() + + return jsonify({ + 'success': True, + 'data': { + 'pharmacy_name': pharmacy_result[0], + 'active_subscriptions': active_subscriptions, + 'available_services': available_services + } + }) + + except Exception as e: + print(f"❌ 앜국 구독 상섞 조회 였류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/subscriptions', methods=['POST']) + def api_create_subscription(): + """새 구독 생성""" + try: + import sqlite3 + from datetime import date, timedelta + + data = request.get_json() + pharmacy_id = data.get('pharmacy_id') + product_id = data.get('product_id') + monthly_fee = data.get('monthly_fee') + notes = data.get('notes', '') + + if not pharmacy_id or not product_id: + return jsonify({'success': False, 'error': '필수 파띌믞터가 누띜되었습니닀.'}), 400 + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 쀑복 구독 확읞 + cursor.execute(''' + SELECT id FROM pharmacy_subscriptions + WHERE pharmacy_id = ? AND product_id = ? AND subscription_status = 'ACTIVE' + ''', (pharmacy_id, product_id)) + + if cursor.fetchone(): + conn.close() + return jsonify({'success': False, 'error': '읎믞 구독 쀑읞 서비슀입니닀.'}), 400 + + # 상품 정볎 조회 + cursor.execute("SELECT monthly_price FROM service_products WHERE id = ?", (product_id,)) + product_result = cursor.fetchone() + if not product_result: + conn.close() + return jsonify({'success': False, 'error': '졎재하지 않는 서비슀입니닀.'}), 404 + + # 월 구독료 섀정 (사용자 지정 또는 Ʞ볞 가격) + if monthly_fee is None: + monthly_fee = product_result[0] + + # 구독 생성 + start_date = date.today() + next_billing_date = start_date + timedelta(days=30) + + cursor.execute(''' + INSERT INTO pharmacy_subscriptions + (pharmacy_id, product_id, subscription_status, start_date, next_billing_date, monthly_fee, notes) + VALUES (?, ?, 'ACTIVE', ?, ?, ?, ?) + ''', (pharmacy_id, product_id, start_date.isoformat(), next_billing_date.isoformat(), monthly_fee, notes)) + + conn.commit() + subscription_id = cursor.lastrowid + conn.close() + + return jsonify({ + 'success': True, + 'message': '구독읎 성공적윌로 생성되었습니닀.', + 'subscription_id': subscription_id + }) + + except Exception as e: + print(f"❌ 구독 생성 였류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/subscriptions/', methods=['DELETE']) + def api_cancel_subscription(subscription_id): + """구독 핎지""" + try: + import sqlite3 + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 구독 졎재 확읞 + cursor.execute("SELECT id FROM pharmacy_subscriptions WHERE id = ?", (subscription_id,)) + if not cursor.fetchone(): + conn.close() + return jsonify({'success': False, 'error': '졎재하지 않는 구독입니닀.'}), 404 + + # 구독 상태륌 CANCELLED로 변겜 + cursor.execute(''' + UPDATE pharmacy_subscriptions + SET subscription_status = 'CANCELLED', updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (subscription_id,)) + + conn.commit() + conn.close() + + return jsonify({ + 'success': True, + 'message': '구독읎 성공적윌로 핎지되었습니닀.' + }) + + except Exception as e: + print(f"❌ 구독 핎지 였류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + # =================== 구독 서비슀 ꎀ늬 페읎지 =================== + + @app.route('/subscriptions') + def subscriptions_page(): + """구독 서비슀 ꎀ늬 페읎지""" + return render_template('subscriptions/list.html') + # 에러 핞듀러 @app.errorhandler(404) def not_found_error(error): diff --git a/farmq-admin/create_subscription_tables.py b/farmq-admin/create_subscription_tables.py new file mode 100644 index 0000000..7e38d82 --- /dev/null +++ b/farmq-admin/create_subscription_tables.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +""" +PharmQ SaaS 구독 서비슀 데읎터베읎슀 테읎랔 생성 슀크늜튞 +""" + +import sqlite3 +import os +from datetime import datetime + +def create_subscription_tables(): + """구독 서비슀 ꎀ렚 테읎랔 생성""" + + # FARMQ 데읎터베읎슀 연결 + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + print("🚀 PharmQ SaaS 구독 서비슀 테읎랔 생성 쀑...") + + try: + # 1. service_products - 서비슀 상품 마슀터 + print("📊 service_products 테읎랔 생성 쀑...") + cursor.execute(''' + CREATE TABLE IF NOT EXISTS service_products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_code VARCHAR(20) UNIQUE NOT NULL, + product_name VARCHAR(100) NOT NULL, + description TEXT, + monthly_price DECIMAL(10,2) NOT NULL, + setup_fee DECIMAL(10,2) DEFAULT 0, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 2. pharmacy_subscriptions - 앜국별 구독 현황 + print("🏥 pharmacy_subscriptions 테읎랔 생성 쀑...") + cursor.execute(''' + CREATE TABLE IF NOT EXISTS pharmacy_subscriptions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pharmacy_id INTEGER NOT NULL, + product_id INTEGER NOT NULL, + subscription_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + start_date DATE NOT NULL, + end_date DATE, + next_billing_date DATE, + monthly_fee DECIMAL(10,2) NOT NULL, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (pharmacy_id) REFERENCES pharmacies(id), + FOREIGN KEY (product_id) REFERENCES service_products(id), + UNIQUE(pharmacy_id, product_id) + ) + ''') + + # 3. subscription_usage_logs - 서비슀 읎용 로귞 + print("📊 subscription_usage_logs 테읎랔 생성 쀑...") + cursor.execute(''' + CREATE TABLE IF NOT EXISTS subscription_usage_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + subscription_id INTEGER NOT NULL, + usage_type VARCHAR(50) NOT NULL, + usage_amount INTEGER DEFAULT 1, + usage_date DATE NOT NULL, + metadata TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (subscription_id) REFERENCES pharmacy_subscriptions(id) + ) + ''') + + # 4. billing_history - 결제 읎력 + print("💳 billing_history 테읎랔 생성 쀑...") + cursor.execute(''' + CREATE TABLE IF NOT EXISTS billing_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + subscription_id INTEGER NOT NULL, + billing_period_start DATE NOT NULL, + billing_period_end DATE NOT NULL, + amount DECIMAL(10,2) NOT NULL, + billing_status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + billing_date DATE, + payment_method VARCHAR(50), + invoice_number VARCHAR(100), + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (subscription_id) REFERENCES pharmacy_subscriptions(id) + ) + ''') + + # 읞덱슀 생성 + print("🔍 읞덱슀 생성 쀑...") + indexes = [ + "CREATE INDEX IF NOT EXISTS idx_pharmacy_subscriptions_pharmacy_id ON pharmacy_subscriptions(pharmacy_id)", + "CREATE INDEX IF NOT EXISTS idx_pharmacy_subscriptions_product_id ON pharmacy_subscriptions(product_id)", + "CREATE INDEX IF NOT EXISTS idx_pharmacy_subscriptions_status ON pharmacy_subscriptions(subscription_status)", + "CREATE INDEX IF NOT EXISTS idx_usage_logs_subscription_id ON subscription_usage_logs(subscription_id)", + "CREATE INDEX IF NOT EXISTS idx_usage_logs_date ON subscription_usage_logs(usage_date)", + "CREATE INDEX IF NOT EXISTS idx_billing_subscription_id ON billing_history(subscription_id)", + "CREATE INDEX IF NOT EXISTS idx_billing_status ON billing_history(billing_status)" + ] + + for index_sql in indexes: + cursor.execute(index_sql) + + conn.commit() + print("✅ 몚든 테읎랔읎 성공적윌로 생성되었습니닀!") + + except Exception as e: + print(f"❌ 였류 발생: {e}") + conn.rollback() + raise + finally: + conn.close() + +def insert_sample_service_products(): + """Ʞ볞 서비슀 상품 데읎터 삜입""" + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + print("📊 Ʞ볞 서비슀 상품 데읎터 삜입 쀑...") + + # Ʞ볞 서비슀 상품 정의 + products = [ + { + 'product_code': 'CLOUD_PC', + 'product_name': '큎띌우드 PC', + 'description': 'Proxmox êž°ë°˜ 가상 데슀크톱 서비슀. ì–žì œ 얎디서나 안전한 큎띌우드 환겜에서 업묎 처늬 가능.', + 'monthly_price': 60000.00, + 'setup_fee': 0.00 + }, + { + 'product_code': 'AI_CCTV', + 'product_name': 'AI CCTV', + 'description': '읞공지능 êž°ë°˜ 볎안 몚니터링 시슀템. 싀시간 읎상 상황 탐지 및 알늌 서비슀.', + 'monthly_price': 80000.00, + 'setup_fee': 50000.00 + }, + { + 'product_code': 'CRM', + 'product_name': 'CRM 시슀템', + 'description': '고객 ꎀ계 ꎀ늬 및 맀출 분석 도구. 고객 데읎터 통합 ꎀ늬 및 마쌀팅 자동화.', + 'monthly_price': 40000.00, + 'setup_fee': 0.00 + } + ] + + try: + for product in products: + # 쀑복 확읞 + cursor.execute("SELECT id FROM service_products WHERE product_code = ?", (product['product_code'],)) + if cursor.fetchone(): + print(f"⚠ {product['product_name']} 상품읎 읎믞 졎재합니닀.") + continue + + cursor.execute(''' + INSERT INTO service_products + (product_code, product_name, description, monthly_price, setup_fee) + VALUES (?, ?, ?, ?, ?) + ''', ( + product['product_code'], + product['product_name'], + product['description'], + product['monthly_price'], + product['setup_fee'] + )) + + print(f"✅ {product['product_name']} 상품 추가됚 (월 {product['monthly_price']:,.0f}원)") + + conn.commit() + print("✅ Ʞ볞 서비슀 상품 데읎터 삜입 완료!") + + except Exception as e: + print(f"❌ 서비슀 상품 데읎터 삜입 였류: {e}") + conn.rollback() + raise + finally: + conn.close() + +def create_sample_subscriptions(): + """샘플 구독 데읎터 생성 (Ʞ졎 앜국 데읎터 êž°ë°˜)""" + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + print("🏥 샘플 구독 데읎터 생성 쀑...") + + try: + # Ʞ졎 앜국 ID 조회 + cursor.execute("SELECT id, pharmacy_name FROM pharmacies LIMIT 10") + pharmacies = cursor.fetchall() + + # 서비슀 상품 ID 조회 + cursor.execute("SELECT id, product_code, monthly_price FROM service_products") + products = cursor.fetchall() + + if not pharmacies: + print("⚠ 앜국 데읎터가 없습니닀. 구독 데읎터륌 생성할 수 없습니닀.") + return + + if not products: + print("⚠ 서비슀 상품 데읎터가 없습니닀.") + return + + import random + from datetime import date, timedelta + + subscription_count = 0 + + for pharmacy_id, pharmacy_name in pharmacies: + # 각 앜국마닀 랜덀하게 1-3개의 서비슀 구독 + num_subscriptions = random.randint(1, 3) + selected_products = random.sample(products, num_subscriptions) + + for product_id, product_code, monthly_price in selected_products: + # 쀑복 구독 확읞 + cursor.execute(''' + SELECT id FROM pharmacy_subscriptions + WHERE pharmacy_id = ? AND product_id = ? + ''', (pharmacy_id, product_id)) + + if cursor.fetchone(): + continue # 읎믞 구독 쀑 + + # 구독 시작음 (최귌 1년 낮 랜덀) + start_date = date.today() - timedelta(days=random.randint(0, 365)) + + # 닀음 결제음 (시작음로부터 월 닚위) + next_billing = start_date + timedelta(days=30) + if next_billing < date.today(): + # 곌거 날짜멎 현재 날짜 Ʞ쀀윌로 조정 + days_since_start = (date.today() - start_date).days + months_passed = days_since_start // 30 + next_billing = start_date + timedelta(days=(months_passed + 1) * 30) + + cursor.execute(''' + INSERT INTO pharmacy_subscriptions + (pharmacy_id, product_id, subscription_status, start_date, + next_billing_date, monthly_fee, notes) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', ( + pharmacy_id, + product_id, + 'ACTIVE', + start_date.isoformat(), + next_billing.isoformat(), + monthly_price, + f'{pharmacy_name}의 {product_code} 서비슀 구독' + )) + + subscription_count += 1 + print(f"✅ {pharmacy_name} → {product_code} 구독 추가 (월 {monthly_price:,.0f}원)") + + conn.commit() + print(f"✅ 쎝 {subscription_count}개의 샘플 구독 데읎터가 생성되었습니닀!") + + except Exception as e: + print(f"❌ 샘플 구독 데읎터 생성 였류: {e}") + conn.rollback() + raise + finally: + conn.close() + +def show_subscription_summary(): + """구독 현황 요앜 출력""" + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + print("\n" + "="*60) + print("📊 PharmQ SaaS 구독 현황 요앜") + print("="*60) + + try: + # 전첎 구독 통계 + cursor.execute(''' + SELECT + sp.product_name, + COUNT(*) as subscription_count, + SUM(ps.monthly_fee) as total_monthly_revenue + FROM pharmacy_subscriptions ps + JOIN service_products sp ON ps.product_id = sp.id + WHERE ps.subscription_status = 'ACTIVE' + GROUP BY sp.product_name + ORDER BY total_monthly_revenue DESC + ''') + + results = cursor.fetchall() + total_revenue = 0 + + for product_name, count, revenue in results: + print(f"🔹 {product_name}: {count}개 앜국 구독 (월 {revenue:,.0f}원)") + total_revenue += revenue + + print(f"\n💰 쎝 월 맀출: {total_revenue:,.0f}원") + + # 앜국별 구독 수 + cursor.execute(''' + SELECT COUNT(DISTINCT pharmacy_id) as subscribed_pharmacies + FROM pharmacy_subscriptions + WHERE subscription_status = 'ACTIVE' + ''') + + subscribed_pharmacies = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM pharmacies") + total_pharmacies = cursor.fetchone()[0] + + print(f"🏥 구독 앜국: {subscribed_pharmacies}/{total_pharmacies}개 ({subscribed_pharmacies/total_pharmacies*100:.1f}%)") + + except Exception as e: + print(f"❌ 요앜 정볎 조회 였류: {e}") + finally: + conn.close() + print("="*60) + +if __name__ == "__main__": + print("🚀 PharmQ SaaS 구독 서비슀 데읎터베읎슀 쎈Ʞ화") + print("-" * 60) + + try: + # 1. 테읎랔 생성 + create_subscription_tables() + print() + + # 2. Ʞ볞 서비슀 상품 삜입 + insert_sample_service_products() + print() + + # 3. 샘플 구독 데읎터 생성 + create_sample_subscriptions() + print() + + # 4. 구독 현황 요앜 + show_subscription_summary() + + print("\n🎉 PharmQ SaaS 구독 서비슀 데읎터베읎슀 쎈Ʞ화 완료!") + + except Exception as e: + print(f"\n💥 쎈Ʞ화 싀팚: {e}") + exit(1) \ No newline at end of file diff --git a/farmq-admin/templates/dashboard/index.html b/farmq-admin/templates/dashboard/index.html index 14c1ce1..10d78e1 100644 --- a/farmq-admin/templates/dashboard/index.html +++ b/farmq-admin/templates/dashboard/index.html @@ -70,6 +70,37 @@ + +
+
+
+
+
+ 구독 서비슀 현황 +
+ + ꎀ늬하Ʞ + +
+
+
+ +
+
+ Loading... +
+
+
+ + +
+ +
+
+
+
+
+
@@ -291,9 +322,132 @@ function updateAlerts() { .catch(error => console.error('Alert update failed:', error)); } +// 구독 현황 로드 +function loadSubscriptionStats() { + fetch('/api/subscriptions/stats') + .then(response => response.json()) + .then(result => { + if (result.success) { + updateSubscriptionDisplay(result.data); + } else { + console.error('구독 통계 로드 싀팚:', result.error); + } + }) + .catch(error => { + console.error('구독 통계 API 였류:', error); + showSubscriptionError(); + }); +} + +function updateSubscriptionDisplay(data) { + // 전첎 구독 통계 칎드 생성 + const statsHtml = ` +
+
+
+
₩${data.total_revenue.toLocaleString()}
+
+ 월 쎝 맀출 +
+
+
+
+
+
+
+
${data.total_subscriptions}
+
+ 쎝 구독 수 +
+
+
+
+
+
+
+
${data.subscribed_pharmacies}/${data.total_pharmacies}
+
+ 구독 앜국 +
+
+
+
+
+
+
+
${data.subscription_rate}%
+
+ 구독률 +
+
+
+
+ `; + + document.getElementById('subscription-stats').innerHTML = statsHtml; + + // 서비슀별 구독 현황 + const serviceIconMap = { + 'CLOUD_PC': '💻', + 'AI_CCTV': '📷', + 'CRM': '📊' + }; + + const serviceColorMap = { + 'CLOUD_PC': '#4f46e5', + 'AI_CCTV': '#0891b2', + 'CRM': '#ca8a04' + }; + + let servicesHtml = ''; + data.services.forEach(service => { + const icon = serviceIconMap[service.code] || '📊'; + const color = serviceColorMap[service.code] || '#6b7280'; + const percentage = data.total_pharmacies > 0 ? Math.round(service.count / data.total_pharmacies * 100) : 0; + + servicesHtml += ` +
+
+
+
+
${icon}
+
+
${service.name}
+
+
+
+ ${service.count}개 앜국 구독 (${percentage}%) +
+
+
+ ₩${service.revenue.toLocaleString()}/월 +
+
+
+
+ `; + }); + + document.getElementById('service-breakdown').innerHTML = servicesHtml; +} + +function showSubscriptionError() { + document.getElementById('subscription-stats').innerHTML = ` +
+ +

구독 현황을 불러였는 쀑 였류가 발생했습니닀.

+
+ `; +} + +// 페읎지 로드 시 구독 현황 로드 +loadSubscriptionStats(); + // 통계 업데읎튞 (10쎈마닀 - 더 자죌) setInterval(updateStats, 10000); // 알늌 업데읎튞 (30쎈마닀) setInterval(updateAlerts, 30000); +// 구독 현황 업데읎튞 (60쎈마닀) +setInterval(loadSubscriptionStats, 60000); {% endblock %} \ No newline at end of file diff --git a/farmq-admin/templates/pharmacy/detail.html b/farmq-admin/templates/pharmacy/detail.html index 8305781..5785825 100644 --- a/farmq-admin/templates/pharmacy/detail.html +++ b/farmq-admin/templates/pharmacy/detail.html @@ -162,6 +162,32 @@
+ +
+
+
+
+
+ 구독 서비슀 현황 +
+ +
+
+
+
+
+ 로딩 쀑... +
+

구독 정볎륌 불러였는 쀑...

+
+
+
+
+
+
+
@@ -315,5 +341,233 @@ function deleteMachine(machineId, machineName) { showToast('ëšžì‹  삭제 쀑 였류가 발생했습니닀.', 'error'); }); } + +// 구독 정볎 로드 +function loadSubscriptionInfo() { + const pharmacyId = {{ pharmacy.id }}; + + fetch(`/api/pharmacy/${pharmacyId}/subscriptions`) + .then(response => response.json()) + .then(result => { + if (result.success) { + displaySubscriptionInfo(result.data); + } else { + showSubscriptionError(); + } + }) + .catch(error => { + console.error('구독 정볎 로드 였류:', error); + showSubscriptionError(); + }); +} + +function displaySubscriptionInfo(data) { + const activeSubscriptions = data.active_subscriptions; + const availableServices = data.available_services; + + let html = ''; + + // 활성 구독 서비슀 + if (activeSubscriptions.length > 0) { + html += ` +
+
+
구독 쀑읞 서비슀
+
+
+
+ `; + + let totalFee = 0; + activeSubscriptions.forEach(sub => { + totalFee += sub.monthly_fee; + + const serviceIcons = { + 'CLOUD_PC': { icon: '💻', color: 'primary' }, + 'AI_CCTV': { icon: '📷', color: 'info' }, + 'CRM': { icon: '📊', color: 'warning' } + }; + + const service = serviceIcons[sub.code] || { icon: '📊', color: 'secondary' }; + + html += ` +
+
+
+
+
${service.icon}
+
+
${sub.name}
+
+ 시작음: ${sub.start_date}
+ 닀음결제: ${sub.next_billing_date} +
+
+
+
+
+ ₩${sub.monthly_fee.toLocaleString()}/월 +
+
+ +
+
+
+
+
+ `; + }); + + html += ` +
+
+
+
+ 쎝 월 구독료: ₩${totalFee.toLocaleString()} +
+
+
+ `; + } + + // 구독 가능한 서비슀 + if (availableServices.length > 0) { + html += ` +
+
+
구독 가능한 서비슀
+
+
+
+ `; + + availableServices.forEach(service => { + const serviceIcons = { + 'CLOUD_PC': { icon: '💻', color: 'primary' }, + 'AI_CCTV': { icon: '📷', color: 'info' }, + 'CRM': { icon: '📊', color: 'warning' } + }; + + const serviceInfo = serviceIcons[service.code] || { icon: '📊', color: 'secondary' }; + + html += ` +
+
+
+
+
${serviceInfo.icon}
+
+
${service.name}
+

${service.description}

+
+
+
+
+ ₩${service.monthly_price.toLocaleString()}/월 +
+
+ +
+
+
+
+
+ `; + }); + + html += '
'; + } + + if (activeSubscriptions.length === 0 && availableServices.length === 0) { + html = ` +
+ +

구독 가능한 서비슀가 없습니닀.

+
+ `; + } + + document.getElementById('subscription-content').innerHTML = html; +} + +function showSubscriptionError() { + document.getElementById('subscription-content').innerHTML = ` +
+ +

구독 정볎륌 불러였는 쀑 였류가 발생했습니닀.

+ +
+ `; +} + +function subscribeService(serviceId, serviceName, monthlyPrice) { + if (!confirm(`"${serviceName}" 서비슀륌 월 ₩${monthlyPrice.toLocaleString()}에 구독하시겠습니까?`)) { + return; + } + + const pharmacyId = {{ pharmacy.id }}; + + fetch('/api/subscriptions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + pharmacy_id: pharmacyId, + product_id: serviceId, + monthly_fee: monthlyPrice + }) + }) + .then(response => response.json()) + .then(result => { + if (result.success) { + showToast(result.message, 'success'); + setTimeout(() => loadSubscriptionInfo(), 1000); + } else { + showToast(result.error, 'error'); + } + }) + .catch(error => { + console.error('구독 생성 였류:', error); + showToast('구독 생성 쀑 였류가 발생했습니닀.', 'error'); + }); +} + +function cancelSubscription(subscriptionId, serviceName) { + if (!confirm(`"${serviceName}" 서비슀 구독을 핎지하시겠습니까?`)) { + return; + } + + fetch(`/api/subscriptions/${subscriptionId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(result => { + if (result.success) { + showToast(result.message, 'success'); + setTimeout(() => loadSubscriptionInfo(), 1000); + } else { + showToast(result.error, 'error'); + } + }) + .catch(error => { + console.error('구독 핎지 였류:', error); + showToast('구독 핎지 쀑 였류가 발생했습니닀.', 'error'); + }); +} + +// 페읎지 로드 시 구독 정볎 로드 +document.addEventListener('DOMContentLoaded', function() { + setTimeout(() => loadSubscriptionInfo(), 500); +}); {% endblock %} \ No newline at end of file diff --git a/farmq-admin/templates/pharmacy/list.html b/farmq-admin/templates/pharmacy/list.html index afe0672..a7427eb 100644 --- a/farmq-admin/templates/pharmacy/list.html +++ b/farmq-admin/templates/pharmacy/list.html @@ -40,6 +40,7 @@ 앜국 정볎 닎당자 + 구독 서비슀 연결된 ëšžì‹  넀튞워크 상태 액션 @@ -68,6 +69,15 @@ {{ pharmacy_data.phone or '연띜처 믞등록' }}
+ +
+
+
+ 로딩... +
+
+
+
{{ pharmacy_data.machine_count }}대 @@ -200,6 +210,9 @@ let pharmacyModal; document.addEventListener('DOMContentLoaded', function() { pharmacyModal = new bootstrap.Modal(document.getElementById('pharmacyModal')); + + // 구독 상태 로드 + setTimeout(loadSubscriptionStatuses, 500); // 페읎지 로드 후 앜간의 지연 }); function showAddModal() { @@ -316,6 +329,94 @@ document.getElementById('pharmacyForm').addEventListener('submit', function(e) { } }); +// 구독 서비슀 상태 로드 +function loadSubscriptionStatuses() { + const subscriptionContainers = document.querySelectorAll('.subscription-services'); + + subscriptionContainers.forEach(container => { + const pharmacyId = container.dataset.pharmacyId; + + fetch(`/api/pharmacy/${pharmacyId}/subscriptions`) + .then(response => response.json()) + .then(result => { + if (result.success) { + displaySubscriptionStatus(container, result.data); + } else { + showSubscriptionError(container); + } + }) + .catch(error => { + console.error(`앜국 ${pharmacyId} 구독 정볎 로드 싀팚:`, error); + showSubscriptionError(container); + }); + }); +} + +function displaySubscriptionStatus(container, data) { + const subscriptions = data.active_subscriptions; + + if (subscriptions.length === 0) { + container.innerHTML = ` +
+ + 구독 없음 + +
+ `; + return; + } + + // 서비슀 아읎윘 맵핑 + const serviceIcons = { + 'CLOUD_PC': { icon: '💻', color: 'primary', name: '큎띌우드PC' }, + 'AI_CCTV': { icon: '📷', color: 'info', name: 'AI CCTV' }, + 'CRM': { icon: '📊', color: 'warning', name: 'CRM' } + }; + + let html = '
'; + let totalFee = 0; + + subscriptions.forEach(sub => { + const service = serviceIcons[sub.code] || { icon: '📊', color: 'secondary', name: sub.name }; + totalFee += sub.monthly_fee; + + html += ` + + ${service.icon} + + `; + }); + + html += '
'; + + // 쎝 월 구독료 표시 + html += ` +
+ ₩${totalFee.toLocaleString()}/월 +
+ `; + + container.innerHTML = html; + + // 툮팁 쎈Ʞ화 + const tooltipTriggerList = container.querySelectorAll('[data-bs-toggle="tooltip"]'); + tooltipTriggerList.forEach(tooltipTriggerEl => { + new bootstrap.Tooltip(tooltipTriggerEl); + }); +} + +function showSubscriptionError(container) { + container.innerHTML = ` +
+ + 였류 + +
+ `; +} + +// 구독 상태 로드 핚수듀은 위의 DOMContentLoaded에서 혞출됚 + // 테읎랔 정렬 및 검색 Ʞ능 추가 (향후) {% endblock %} \ No newline at end of file diff --git a/fix-database-constraints.py b/fix-database-constraints.py new file mode 100755 index 0000000..3d6f7bd --- /dev/null +++ b/fix-database-constraints.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +""" +Headscale 데읎터베읎슀 왞래킀 제앜조걎 수정 슀크늜튞 +""" + +import sqlite3 +import os +from datetime import datetime + +def fix_database_constraints(): + """왞래킀 제앜조걎 묞제 핎결""" + db_path = '/srv/headscale-setup/data/db.sqlite' + backup_path = f'/srv/headscale-setup/data/db.sqlite.backup.{datetime.now().strftime("%Y%m%d_%H%M%S")}' + + print("🔧 Headscale 데읎터베읎슀 왞래킀 제앜조걎 수정") + print("=" * 60) + + # 1. 백업 생성 + print(f"📊 데읎터베읎슀 백업 쀑: {backup_path}") + import shutil + shutil.copy2(db_path, backup_path) + print("✅ 백업 완료") + + # 2. 데읎터베읎슀 연결 + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + try: + # 3. 왞래킀 제앜조걎 비활성화 + print("🔓 왞래킀 제앜조걎 비활성화 쀑...") + cursor.execute("PRAGMA foreign_keys = OFF") + + # 4. Ʞ졎 테읎랔듀 확읞 + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%pharmacy%'") + pharmacy_tables = cursor.fetchall() + print(f"📋 발견된 앜국 ꎀ렚 테읎랔: {[table[0] for table in pharmacy_tables]}") + + # 5. pharmacy_info 테읎랔 재생성 (왞래킀 없읎) + if any('pharmacy_info' in table[0] for table in pharmacy_tables): + print("🔄 pharmacy_info 테읎랔 재생성 쀑...") + + # Ʞ졎 데읎터 백업 + cursor.execute("SELECT * FROM pharmacy_info") + existing_data = cursor.fetchall() + + # Ʞ졎 테읎랔 구조 확읞 + cursor.execute("PRAGMA table_info(pharmacy_info)") + columns = cursor.fetchall() + print(f"📊 Ʞ졎 컬럌: {[col[1] for col in columns]}") + + # 새 테읎랔 생성 (왞래킀 제앜조걎 없음) + cursor.execute("DROP TABLE IF EXISTS pharmacy_info_new") + cursor.execute(""" + CREATE TABLE pharmacy_info_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pharmacy_name VARCHAR(255) NOT NULL, + business_number VARCHAR(20), + manager_name VARCHAR(100), + phone VARCHAR(20), + address TEXT, + proxmox_host VARCHAR(255), + user_id VARCHAR(255), -- 왞래킀 제앜조걎 제거 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + # Ʞ졎 데읎터 복사 + if existing_data: + print(f"📁 Ʞ졎 데읎터 복사 쀑... ({len(existing_data)}개 레윔드)") + cursor.executemany(""" + INSERT INTO pharmacy_info_new + (id, pharmacy_name, business_number, manager_name, phone, address, proxmox_host, user_id, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, existing_data) + + # Ʞ졎 테읎랔 삭제 및 새 테읎랔로 교첎 + cursor.execute("DROP TABLE pharmacy_info") + cursor.execute("ALTER TABLE pharmacy_info_new RENAME TO pharmacy_info") + print("✅ pharmacy_info 테읎랔 재생성 완료") + + # 6. machine_specs 테읎랔도 수정 (필요시) + if any('machine_specs' in table[0] for table in pharmacy_tables): + print("🔄 machine_specs 테읎랔 확읞 쀑...") + + cursor.execute("PRAGMA table_info(machine_specs)") + columns = cursor.fetchall() + + # 왞래킀 제앜조걎읎 있는지 확읞 + cursor.execute("SELECT sql FROM sqlite_master WHERE name='machine_specs'") + table_sql = cursor.fetchone() + + if table_sql and 'REFERENCES' in table_sql[0]: + print("🔄 machine_specs 테읎랔 재생성 쀑...") + + # Ʞ졎 데읎터 백업 + cursor.execute("SELECT * FROM machine_specs") + existing_specs = cursor.fetchall() + + # 새 테읎랔 생성 (왞래킀 제앜조걎 없음) + cursor.execute("DROP TABLE IF EXISTS machine_specs_new") + cursor.execute(""" + CREATE TABLE machine_specs_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + machine_id INTEGER, -- 왞래킀 제앜조걎 제거 + pharmacy_id INTEGER, -- 왞래킀 제앜조걎 제거 + cpu_model VARCHAR(255), + cpu_cores INTEGER, + ram_gb INTEGER, + storage_gb INTEGER, + network_speed INTEGER, + os_info VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + # Ʞ졎 데읎터 복사 + if existing_specs: + print(f"📁 Ʞ졎 슀펙 데읎터 복사 쀑... ({len(existing_specs)}개 레윔드)") + cursor.executemany(""" + INSERT INTO machine_specs_new + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, existing_specs) + + # Ʞ졎 테읎랔 삭제 및 새 테읎랔로 교첎 + cursor.execute("DROP TABLE machine_specs") + cursor.execute("ALTER TABLE machine_specs_new RENAME TO machine_specs") + print("✅ machine_specs 테읎랔 재생성 완료") + + # 7. 변겜사항 컀밋 + conn.commit() + print("💟 변겜사항 저장 완료") + + # 8. 왞래킀 제앜조걎 재활성화 (필요시) + cursor.execute("PRAGMA foreign_keys = ON") + + # 9. 묎결성 검사 + print("🔍 데읎터베읎슀 묎결성 검사 쀑...") + cursor.execute("PRAGMA integrity_check") + integrity_result = cursor.fetchone() + print(f"✅ 묎결성 검사 결곌: {integrity_result[0]}") + + print("\n🎉 데읎터베읎슀 수정 완료!") + print(f"📊 백업 위치: {backup_path}") + print("🚀 읎제 Tailscale 큎띌읎얞튞 등록을 닀시 시도핎볎섞요!") + + except Exception as e: + print(f"❌ 였류 발생: {e}") + conn.rollback() + print(f"🔙 백업에서 복원하렀멎: cp {backup_path} {db_path}") + raise + finally: + conn.close() + +if __name__ == '__main__': + fix_database_constraints() \ No newline at end of file diff --git a/quick-fix-db.py b/quick-fix-db.py new file mode 100644 index 0000000..afdb35d --- /dev/null +++ b/quick-fix-db.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +""" +빠륞 데읎터베읎슀 수정 - 왞래킀 제앜조걎만 제거 +""" + +import sqlite3 +import os +from datetime import datetime + +def quick_fix(): + """빠륎게 왞래킀 제앜조걎만 비활성화""" + db_path = '/srv/headscale-setup/data/db.sqlite' + + print("🔧 빠륞 수정: 왞래킀 제앜조걎 비활성화") + print("=" * 50) + + # Docker 컚테읎너 쀑지 + print("⏹ Headscale 컚테읎너 쀑지 쀑...") + os.system("cd /srv/headscale-setup && docker-compose stop headscale") + + # 데읎터베읎슀 연결 + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + try: + # 왞래킀 제앜조걎 영구 비활성화 + print("🔓 왞래킀 제앜조걎 비활성화...") + cursor.execute("PRAGMA foreign_keys = OFF") + + # 섀정 확읞 + cursor.execute("PRAGMA foreign_keys") + fk_status = cursor.fetchone()[0] + print(f"✅ 왞래킀 상태: {'비활성화' if fk_status == 0 else '활성화'}") + + # 테읎랔 확읞 + cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = cursor.fetchall() + print(f"📋 테읎랔 목록: {[table[0] for table in tables]}") + + conn.commit() + print("💟 섀정 저장 완료") + + except Exception as e: + print(f"❌ 였류: {e}") + conn.rollback() + finally: + conn.close() + + # Docker 컚테읎너 재시작 + print("🚀 Headscale 컚테읎너 재시작 쀑...") + os.system("cd /srv/headscale-setup && docker-compose start headscale") + + print("✅ 완료! 읎제 Tailscale 큎띌읎얞튞 등록을 닀시 시도핎볎섞요.") + +if __name__ == '__main__': + quick_fix() \ No newline at end of file diff --git a/test-proxmox-api.py b/test-proxmox-api.py new file mode 100755 index 0000000..0b3be6a --- /dev/null +++ b/test-proxmox-api.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +""" +Proxmox VE API 테슀튞 슀크늜튞 +사용법: python test-proxmox-api.py [root-password] +""" + +import requests +import json +import sys +import urllib3 +from urllib.parse import quote_plus + +# SSL 겜고 묎시 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +def test_api_token_method(host, token): + """API Token 방식 테슀튞""" + print("🔑 API Token 방식 테슀튞...") + + headers = { + 'Authorization': f'PVEAPIToken={token}' + } + + try: + response = requests.get( + f'https://{host}:443/api2/json/version', + headers=headers, + verify=False, + timeout=10 + ) + + if response.status_code == 200: + version_info = response.json()['data'] + print(f"✅ API Token 읞슝 성공!") + print(f" Proxmox Version: {version_info['version']}") + print(f" Release: {version_info['release']}") + return True + else: + print(f"❌ API Token 읞슝 싀팚: {response.status_code}") + print(f" 응답: {response.text}") + return False + + except Exception as e: + print(f"❌ API Token 연결 싀팚: {e}") + return False + +def test_session_method(host, username, password): + """섞션 ì¿ í‚€ 방식 테슀튞""" + print("🍪 섞션 ì¿ í‚€ 방식 테슀튞...") + + # 1닚계: 로귞읞하여 티쌓 획득 + login_data = { + 'username': username, + 'password': password + } + + try: + response = requests.post( + f'https://{host}:443/api2/json/access/ticket', + data=login_data, + verify=False, + timeout=10 + ) + + if response.status_code != 200: + print(f"❌ 로귞읞 싀팚: {response.status_code}") + print(f" 응답: {response.text}") + return False, None, None + + data = response.json()['data'] + ticket = data['ticket'] + csrf_token = data['CSRFPreventionToken'] + + print("✅ 로귞읞 성공!") + print(f" 티쌓: {ticket[:20]}...") + print(f" CSRF: {csrf_token}") + + return True, ticket, csrf_token + + except Exception as e: + print(f"❌ 로귞읞 연결 싀팚: {e}") + return False, None, None + +def test_vm_list_api(host, ticket=None, csrf_token=None, api_token=None): + """VM 목록 조회 API 테슀튞""" + print("\n📋 VM 목록 조회 API 테슀튞...") + + if api_token: + headers = {'Authorization': f'PVEAPIToken={api_token}'} + cookies = None + else: + headers = {'CSRFPreventionToken': csrf_token} + cookies = {'PVEAuthCookie': ticket} + + try: + response = requests.get( + f'https://{host}:443/api2/json/cluster/resources?type=vm', + headers=headers, + cookies=cookies, + verify=False, + timeout=10 + ) + + if response.status_code == 200: + vms = response.json()['data'] + print(f"✅ VM 목록 조회 성공! (쎝 {len(vms)}개)") + + for vm in vms: + status_icon = "🟢" if vm.get('status') == 'running' else "🔎" + print(f" {status_icon} VM {vm.get('vmid')}: {vm.get('name', 'N/A')} ({vm.get('status', 'unknown')})") + + return True, vms + else: + print(f"❌ VM 목록 조회 싀팚: {response.status_code}") + print(f" 응답: {response.text}") + return False, [] + + except Exception as e: + print(f"❌ VM 목록 API 연결 싀팚: {e}") + return False, [] + +def test_vnc_proxy_api(host, node, vmid, ticket=None, csrf_token=None, api_token=None): + """VNC 프록시 티쌓 생성 테슀튞""" + print(f"\n🖥 VNC 프록시 API 테슀튞 (VM {vmid})...") + + if api_token: + headers = {'Authorization': f'PVEAPIToken={api_token}'} + cookies = None + else: + headers = {'CSRFPreventionToken': csrf_token} + cookies = {'PVEAuthCookie': ticket} + + data = {'websocket': '1'} + + try: + response = requests.post( + f'https://{host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncproxy', + headers=headers, + cookies=cookies, + data=data, + verify=False, + timeout=10 + ) + + if response.status_code == 200: + vnc_data = response.json()['data'] + print("✅ VNC 프록시 티쌓 생성 성공!") + print(f" 포튞: {vnc_data['port']}") + print(f" 티쌓: {vnc_data['ticket'][:20]}...") + + # WebSocket URL 생성 + encoded_ticket = quote_plus(vnc_data['ticket']) + ws_url = f"wss://{host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}" + print(f" WebSocket URL: {ws_url[:80]}...") + + return True, vnc_data + else: + print(f"❌ VNC 프록시 생성 싀팚: {response.status_code}") + print(f" 응답: {response.text}") + return False, None + + except Exception as e: + print(f"❌ VNC 프록시 API 연결 싀팚: {e}") + return False, None + +def main(): + if len(sys.argv) < 2: + print("사용법: python test-proxmox-api.py [root-password] [api-token]") + print() + print("예시:") + print(" python test-proxmox-api.py 100.64.0.1 mypassword") + print(" python test-proxmox-api.py 100.64.0.1 '' root@pam!token=uuid") + sys.exit(1) + + host = sys.argv[1] + password = sys.argv[2] if len(sys.argv) > 2 else None + api_token = sys.argv[3] if len(sys.argv) > 3 else None + + print("=" * 60) + print(f"🚀 Proxmox API 테슀튞 - {host}") + print("=" * 60) + + # API Token 방식 테슀튞 + if api_token: + success = test_api_token_method(host, api_token) + if success: + # VM 목록 테슀튞 + vm_success, vms = test_vm_list_api(host, api_token=api_token) + + # 싀행 쀑읞 VM읎 있윌멎 VNC 테슀튞 + if vm_success and vms: + running_vm = next((vm for vm in vms if vm.get('status') == 'running'), None) + if running_vm: + test_vnc_proxy_api(host, running_vm['node'], running_vm['vmid'], api_token=api_token) + else: + print("⚠ 싀행 쀑읞 VM읎 없얎 VNC 테슀튞륌 걎너뜁니닀.") + + # 팚슀워드 방식 테슀튞 + elif password: + login_success, ticket, csrf_token = test_session_method(host, 'root@pam', password) + + if login_success: + # VM 목록 테슀튞 + vm_success, vms = test_vm_list_api(host, ticket, csrf_token) + + # 싀행 쀑읞 VM읎 있윌멎 VNC 테슀튞 + if vm_success and vms: + running_vm = next((vm for vm in vms if vm.get('status') == 'running'), None) + if running_vm: + test_vnc_proxy_api(host, running_vm['node'], running_vm['vmid'], ticket, csrf_token) + else: + print("⚠ 싀행 쀑읞 VM읎 없얎 VNC 테슀튞륌 걎너뜁니닀.") + + else: + print("❌ 팚슀워드 또는 API 토큰을 제공핎죌섞요.") + sys.exit(1) + + print("\n" + "=" * 60) + print("✅ 테슀튞 완료!") + +if __name__ == '__main__': + main() \ No newline at end of file