📋 기획 및 설계: - 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 <noreply@anthropic.com>
156 lines
6.7 KiB
Python
Executable File
156 lines
6.7 KiB
Python
Executable File
#!/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() |