# Headscale 자동 등록 및 DB 생성 개선 기획서 ## 📅 작성일 **2025년 11월 14일** --- ## 🎯 목표 Headscale 설치 스크립트(`headscale-quick-install.sh`)를 개선하여: 1. ✅ Headscale VPN 등록 (현재 완료) 2. ✅ **farmq.db 자동 생성** (신규) 3. ✅ **gateway.db 자동 생성** (신규) 4. ✅ **즉시 프론트엔드 로그인 가능** (신규) **최종 결과:** 스크립트 실행 → 즉시 React 프론트엔드에서 로그인하여 사용 가능 --- ## 📊 현재 상태 분석 ### 현재 스크립트 흐름 ``` 1. OS 감지 2. Tailscale 설치 3. Tailscale 서비스 시작 4. Headscale 등록 (preauth key 사용) 5. VPN IP 할당 받음 (예: 100.64.0.15) 6. 연결 확인 7. 종료 ❌ (DB 생성 없음) ``` ### 문제점 - ❌ farmq.db에 약국 정보 없음 - ❌ gateway.db에 사용자 계정 없음 - ❌ 프론트엔드에서 로그인 불가능 - ❌ 수동으로 DB 수정 필요 --- ## 🔄 개선된 흐름 ### 새로운 스크립트 흐름 ``` 1. OS 감지 2. Tailscale 설치 3. Tailscale 서비스 시작 4. Headscale 등록 (preauth key 사용) 5. VPN IP 할당 받음 (예: 100.64.0.15) 6. 연결 확인 7. ✨ 약국 정보 입력 받기 (대화형) 8. ✨ farmq.db에 약국 등록 9. ✨ gateway.db에 사용자 생성 10. ✨ 로그인 정보 출력 11. 종료 ✅ (완전 자동화) ``` --- ## 🗄️ 데이터베이스 맵핑 구조 ### 3계층 DB 구조 ``` ┌─────────────────────────────────────────────────────────┐ │ gateway.db │ │ ┌───────────────────────────────────────────────────┐ │ │ │ users │ │ │ │ - username: "약국코드_admin" (예: P0004_admin) │ │ │ │ - password_hash: (자동 생성된 비밀번호) │ │ │ │ - primary_pharmacy_code: "P0004" ◄───────┐ │ │ │ │ - role: "admin" │ │ │ │ │ - email: "p0004@pharmq.kr" │ │ │ │ └───────────────────────────────────────────┼───────┘ │ │ │ │ │ ┌───────────────────────────────────────────┼───────┐ │ │ │ pharmacy_members │ │ │ │ │ - user_id: (위에서 생성된 ID) │ │ │ │ │ - pharmacy_code: "P0004" ◄───────────────┘ │ │ │ │ - position: "대표약사" │ │ │ │ - access_level: "owner" │ │ │ └───────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ↓ pharmacy_code ┌─────────────────────────────────────────────────────────┐ │ farmq.db │ │ ┌───────────────────────────────────────────────────┐ │ │ │ pharmacies │ │ │ │ - pharmacy_code: "P0004" ◄─────────────────┐ │ │ │ │ - pharmacy_name: "사용자 입력" │ │ │ │ │ - tailscale_ip: "100.64.0.15" (Headscale) │ │ │ │ │ - api_port: 8082 │ │ │ │ │ - status: "active" │ │ │ │ │ - owner_name: "사용자 입력" │ │ │ │ │ - owner_phone: "사용자 입력" │ │ │ │ │ - headscale_user_name: "default" │ │ │ │ └───────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ↓ vpn_ip ┌─────────────────────────────────────────────────────────┐ │ db.sqlite (Headscale - 읽기전용) │ │ ┌───────────────────────────────────────────────────┐ │ │ │ nodes │ │ │ │ - ipv4: "100.64.0.15" │ │ │ │ - last_seen: "실시간" │ │ │ │ - user_id: 1 (default) │ │ │ └───────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` --- ## 📝 사용자 입력 항목 ### 필수 입력 항목 ```bash 1. 약국 이름 (pharmacy_name) 예: "행복약국", "새서울약국" 2. 대표약사 이름 (owner_name) 예: "홍길동" 3. 대표약사 전화번호 (owner_phone) 예: "010-1234-5678" 4. 이메일 (owner_email) - 선택 예: "happy@pharmq.kr" ``` ### 자동 생성 항목 ```bash 1. pharmacy_code 로직: farmq.db에서 가장 큰 번호 + 1 예: 마지막이 P0002 → P0003 생성 2. VPN IP (tailscale_ip) 로직: tailscale ip -4 명령으로 자동 획득 예: 100.64.0.15 3. username 로직: {pharmacy_code}_admin 예: P0003 → "p0003_admin" 4. password 로직: 랜덤 8자리 생성 (영문+숫자) 예: "aB3xK9mP" 5. email (입력 없을 시) 로직: {pharmacy_code}@pharmq.kr 예: "p0003@pharmq.kr" ``` --- ## 🔧 기술 구현 방안 ### 1. DB 접근 방법 #### Option A: SQLite CLI (간단) ```bash sqlite3 /srv/headscale-tailscale-replacement/farmq-admin/farmq.db \ "INSERT INTO pharmacies (pharmacy_code, pharmacy_name, ...) VALUES (...);" ``` **장점:** - ✅ 추가 의존성 없음 - ✅ 스크립트에 바로 통합 가능 **단점:** - ❌ 복잡한 쿼리 작성 어려움 - ❌ 에러 핸들링 제한적 #### Option B: Python 스크립트 (권장 ⭐) ```bash python3 /srv/pharmq-gateway/scripts/register_pharmacy.py \ --name "행복약국" \ --owner "홍길동" \ --phone "010-1234-5678" \ --vpn-ip "100.64.0.15" ``` **장점:** - ✅ 복잡한 로직 구현 가능 - ✅ 에러 핸들링 우수 - ✅ 검증 로직 추가 가능 - ✅ 비밀번호 해싱 자동 **단점:** - ❌ Python 의존성 필요 (대부분 시스템에 기본 설치됨) ### 2. 스크립트 구조 ```bash # headscale-quick-install.sh 개선안 main() { # 기존 코드 (1-6단계) detect_os check_requirements install_tailscale start_tailscale register_headscale verify_connection # ✨ 신규 추가 (7-10단계) collect_pharmacy_info # 약국 정보 입력 받기 register_to_farmq_db # farmq.db 등록 create_gateway_user # gateway.db 사용자 생성 show_login_info # 로그인 정보 출력 # 기존 마무리 cleanup show_final_info } ``` --- ## 📋 상세 구현 계획 ### Step 7: 약국 정보 수집 (대화형) ```bash collect_pharmacy_info() { print_header "약국 정보 입력" # VPN IP 자동 획득 TAILSCALE_IP=$(tailscale ip -4 2>/dev/null) print_info "할당된 VPN IP: $TAILSCALE_IP" # 다음 약국 코드 자동 생성 NEXT_CODE=$(get_next_pharmacy_code) print_info "새 약국 코드: $NEXT_CODE" # 사용자 입력 read -p "약국 이름을 입력하세요: " PHARMACY_NAME read -p "대표약사 이름을 입력하세요: " OWNER_NAME read -p "대표약사 전화번호 (010-XXXX-XXXX): " OWNER_PHONE read -p "이메일 (선택, Enter로 건너뛰기): " OWNER_EMAIL # 기본값 설정 if [ -z "$OWNER_EMAIL" ]; then OWNER_EMAIL="${NEXT_CODE,,}@pharmq.kr" # 소문자로 변환 fi # 확인 print_info "입력 정보 확인:" echo " 약국 코드: $NEXT_CODE" echo " 약국 이름: $PHARMACY_NAME" echo " 대표약사: $OWNER_NAME" echo " 전화번호: $OWNER_PHONE" echo " 이메일: $OWNER_EMAIL" echo " VPN IP: $TAILSCALE_IP" read -p "위 정보로 등록하시겠습니까? (Y/n): " CONFIRM if [[ ! $CONFIRM =~ ^[Yy]$ ]] && [ -n "$CONFIRM" ]; then print_error "등록이 취소되었습니다." exit 1 fi } ``` ### Step 8: farmq.db 등록 #### 8-1. Python 헬퍼 스크립트 생성 **파일:** `/srv/pharmq-gateway/scripts/register_pharmacy.py` ```python #!/usr/bin/env python3 """ 약국 자동 등록 스크립트 Headscale 설치 후 farmq.db와 gateway.db에 약국 정보 자동 생성 """ import sqlite3 import sys import argparse from datetime import datetime import secrets import string from passlib.hash import pbkdf2_sha256 def generate_password(length=8): """랜덤 비밀번호 생성""" alphabet = string.ascii_letters + string.digits return ''.join(secrets.choice(alphabet) for _ in range(length)) def get_next_pharmacy_code(farmq_db_path): """다음 약국 코드 생성 (P0001, P0002, ...)""" conn = sqlite3.connect(farmq_db_path) cursor = conn.cursor() cursor.execute(""" SELECT pharmacy_code FROM pharmacies WHERE pharmacy_code LIKE 'P%' ORDER BY pharmacy_code DESC LIMIT 1 """) result = cursor.fetchone() conn.close() if result: # P0002 → 2 → 3 → P0003 last_num = int(result[0][1:]) next_num = last_num + 1 else: next_num = 1 return f"P{next_num:04d}" def register_pharmacy(args): """farmq.db에 약국 등록""" print("=" * 60) print("📋 farmq.db 약국 등록") print("=" * 60) # pharmacy_code 생성 if not args.code: args.code = get_next_pharmacy_code(args.farmq_db) print(f"\n약국 코드: {args.code}") print(f"약국 이름: {args.name}") print(f"VPN IP: {args.vpn_ip}") conn = sqlite3.connect(args.farmq_db) cursor = conn.cursor() try: cursor.execute(""" INSERT INTO pharmacies ( pharmacy_code, pharmacy_name, tailscale_ip, api_port, status, owner_name, owner_phone, owner_email, headscale_user_name, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( args.code, args.name, args.vpn_ip, 8082, # 기본 API 포트 'active', args.owner, args.phone, args.email or f"{args.code.lower()}@pharmq.kr", 'default', # Headscale user datetime.now().isoformat(), datetime.now().isoformat() )) conn.commit() print(f"\n✅ farmq.db 등록 완료 (pharmacy_code: {args.code})") except sqlite3.IntegrityError as e: print(f"\n❌ 오류: {e}") print(f"약국 코드 '{args.code}'가 이미 존재합니다.") sys.exit(1) finally: conn.close() return args.code def create_gateway_user(args, pharmacy_code): """gateway.db에 사용자 생성""" print("\n" + "=" * 60) print("🔐 gateway.db 사용자 생성") print("=" * 60) # username 생성 username = f"{pharmacy_code.lower()}_admin" # 랜덤 비밀번호 생성 password = generate_password(8) password_hash = pbkdf2_sha256.hash(password) print(f"\nUsername: {username}") print(f"Password: {password}") conn = sqlite3.connect(args.gateway_db) cursor = conn.cursor() try: # users 테이블에 삽입 cursor.execute(""" INSERT INTO users ( username, email, password_hash, name, phone, primary_pharmacy_code, role, status, failed_login_attempts, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( username, args.email or f"{pharmacy_code.lower()}@pharmq.kr", password_hash, args.owner, args.phone, pharmacy_code, 'admin', 'active', 0, datetime.now().isoformat(), datetime.now().isoformat() )) user_id = cursor.lastrowid # pharmacy_members 테이블에 삽입 cursor.execute(""" INSERT INTO pharmacy_members ( pharmacy_code, user_id, position, access_level, can_view_sales, can_view_inventory, can_manage_staff, is_active, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( pharmacy_code, user_id, '대표약사', 'owner', True, True, True, True, datetime.now().isoformat(), datetime.now().isoformat() )) conn.commit() print(f"\n✅ gateway.db 사용자 생성 완료 (user_id: {user_id})") except sqlite3.IntegrityError as e: print(f"\n❌ 오류: {e}") sys.exit(1) finally: conn.close() return username, password def main(): parser = argparse.ArgumentParser(description='약국 자동 등록') parser.add_argument('--name', required=True, help='약국 이름') parser.add_argument('--owner', required=True, help='대표약사 이름') parser.add_argument('--phone', required=True, help='전화번호') parser.add_argument('--vpn-ip', required=True, help='VPN IP 주소') parser.add_argument('--email', help='이메일 (선택)') parser.add_argument('--code', help='약국 코드 (자동 생성)') parser.add_argument('--farmq-db', default='/srv/headscale-tailscale-replacement/farmq-admin/farmq.db', help='farmq.db 경로') parser.add_argument('--gateway-db', default='/srv/pharmq-gateway/gateway.db', help='gateway.db 경로') args = parser.parse_args() # 1. farmq.db 등록 pharmacy_code = register_pharmacy(args) # 2. gateway.db 사용자 생성 username, password = create_gateway_user(args, pharmacy_code) # 3. 최종 정보 출력 print("\n" + "=" * 60) print("✅ 등록 완료!") print("=" * 60) print(f"\n📋 약국 정보:") print(f" 약국 코드: {pharmacy_code}") print(f" 약국 이름: {args.name}") print(f" VPN IP: {args.vpn_ip}") print(f" API 포트: 8082") print(f"\n🔑 로그인 정보:") print(f" Username: {username}") print(f" Password: {password}") print(f" 이메일: {args.email or f'{pharmacy_code.lower()}@pharmq.kr'}") print(f"\n🌐 프론트엔드 접속:") print(f" https://pharmq.kr") print(f" 또는") print(f" https://dev.pharmq.kr") print("\n⚠️ 비밀번호를 안전한 곳에 보관하세요!") print("=" * 60) if __name__ == '__main__': main() ``` #### 8-2. Bash 스크립트에서 호출 ```bash register_to_farmq_and_gateway() { print_status "약국 정보 등록 중..." # Python 스크립트 실행 REGISTER_OUTPUT=$(python3 /srv/pharmq-gateway/scripts/register_pharmacy.py \ --name "$PHARMACY_NAME" \ --owner "$OWNER_NAME" \ --phone "$OWNER_PHONE" \ --email "$OWNER_EMAIL" \ --vpn-ip "$TAILSCALE_IP" 2>&1) if [ $? -eq 0 ]; then print_success "등록 완료!" echo "$REGISTER_OUTPUT" # 로그인 정보 추출 USERNAME=$(echo "$REGISTER_OUTPUT" | grep "Username:" | awk '{print $2}') PASSWORD=$(echo "$REGISTER_OUTPUT" | grep "Password:" | awk '{print $2}') PHARMACY_CODE=$(echo "$REGISTER_OUTPUT" | grep "약국 코드:" | awk '{print $3}') # 환경 변수 저장 export PHARMACY_CODE export USERNAME export PASSWORD else print_error "등록 실패!" echo "$REGISTER_OUTPUT" exit 1 fi } ``` --- ## 🎨 최종 사용자 경험 ### 실행 예시 ```bash # 1. 스크립트 실행 curl -fsSL https://git.0bin.in/thug0bin/pve9-repo-fix/raw/branch/main/headscale-quick-install.sh | bash # 2. Headscale 등록 자동 진행... # 3. VPN IP 할당: 100.64.0.15 # 4. 약국 정보 입력 ============================================ 약국 정보 입력 ============================================ 할당된 VPN IP: 100.64.0.15 새 약국 코드: P0004 약국 이름을 입력하세요: 행복약국 대표약사 이름을 입력하세요: 홍길동 대표약사 전화번호 (010-XXXX-XXXX): 010-1234-5678 이메일 (선택, Enter로 건너뛰기): 입력 정보 확인: 약국 코드: P0004 약국 이름: 행복약국 대표약사: 홍길동 전화번호: 010-1234-5678 이메일: p0004@pharmq.kr VPN IP: 100.64.0.15 위 정보로 등록하시겠습니까? (Y/n): Y # 5. 자동 등록 진행... ============================================ 설치 완료! ============================================ ✅ Headscale VPN 연결 완료 ✅ farmq.db 약국 등록 완료 ✅ gateway.db 사용자 생성 완료 📋 약국 정보: 약국 코드: P0004 약국 이름: 행복약국 VPN IP: 100.64.0.15 API 포트: 8082 🔑 로그인 정보: Username: p0004_admin Password: aB3xK9mP 이메일: p0004@pharmq.kr 🌐 프론트엔드 접속: https://pharmq.kr 또는 https://dev.pharmq.kr ⚠️ 비밀번호를 안전한 곳에 보관하세요! ============================================ ``` ### 프론트엔드 로그인 ``` 1. https://pharmq.kr 접속 2. Username: p0004_admin 3. Password: aB3xK9mP 4. 로그인 → 즉시 사용 가능! ✅ ``` --- ## ⚙️ 구현 우선순위 ### Phase 1: Python 스크립트 생성 ⭐ (최우선) ```bash /srv/pharmq-gateway/scripts/register_pharmacy.py ``` - farmq.db INSERT - gateway.db users, pharmacy_members INSERT - 비밀번호 해싱 - 에러 핸들링 ### Phase 2: Bash 스크립트 통합 ⭐ ```bash headscale-quick-install.sh 수정 ``` - collect_pharmacy_info() 함수 추가 - register_to_farmq_and_gateway() 함수 추가 - show_login_info() 함수 추가 ### Phase 3: 테스트 ⭐ ```bash 1. 새 VM에서 스크립트 실행 2. DB 확인 3. 프론트엔드 로그인 테스트 4. API 통신 테스트 ``` ### Phase 4: 문서화 ```markdown - README 업데이트 - 스크립트 주석 추가 - 트러블슈팅 가이드 ``` --- ## 🛡️ 보안 고려사항 ### 1. 비밀번호 생성 - ✅ 8자리 이상 - ✅ 영문 대소문자 + 숫자 조합 - ✅ `secrets` 모듈 사용 (암호학적으로 안전) ### 2. 비밀번호 해싱 - ✅ pbkdf2_sha256 사용 - ✅ Salt 자동 생성 - ✅ Rainbow table 공격 방지 ### 3. DB 접근 권한 - ✅ root 권한으로 스크립트 실행 필요 - ✅ DB 파일 권한 확인 - ✅ SQLite injection 방지 (parameterized query) ### 4. 로그인 정보 노출 - ⚠️ 스크립트 실행 화면에 비밀번호 표시됨 - ✅ 일회용 비밀번호 생성 - ✅ 초기 로그인 후 비밀번호 변경 권장 --- ## 📊 데이터 흐름 다이어그램 ``` ┌─────────────────────────────────────────────────────────────┐ │ 사용자 (터미널) │ └─────────────────┬───────────────────────────────────────────┘ │ │ 1. 스크립트 실행 ↓ ┌─────────────────────────────────────────────────────────────┐ │ headscale-quick-install.sh │ ├─────────────────────────────────────────────────────────────┤ │ 1. OS 감지 │ │ 2. Tailscale 설치 │ │ 3. Headscale 등록 │ │ 4. VPN IP 획득: 100.64.0.15 │ │ 5. 약국 정보 입력 받기 ────────────┐ │ │ │ │ │ 6. Python 스크립트 호출 ───────────┼──────────┐ │ └─────────────────────────────────────┼──────────┼────────────┘ │ │ ┌───────────────────┘ │ │ │ ↓ ↓ ┌─────────────────────────────────┐ ┌─────────────────────────┐ │ register_pharmacy.py │ │ register_pharmacy.py │ ├─────────────────────────────────┤ ├─────────────────────────┤ │ 1. 다음 코드 생성: P0004 │ │ 1. username 생성 │ │ 2. farmq.db INSERT │ │ p0004_admin │ │ - pharmacy_code: P0004 │ │ 2. password 생성 │ │ - pharmacy_name: 행복약국 │ │ aB3xK9mP │ │ - tailscale_ip: 100.64.0.15 │ │ 3. password 해싱 │ │ - api_port: 8082 │ │ 4. gateway.db INSERT │ │ - status: active │ │ - users 테이블 │ │ 3. 성공 반환 │ │ - pharmacy_members │ └─────────────────┬───────────────┘ └──────────┬──────────────┘ │ │ │ │ └──────────┬──────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 최종 정보 출력 │ ├─────────────────────────────────────────────────────────────┤ │ 약국 코드: P0004 │ │ 약국 이름: 행복약국 │ │ VPN IP: 100.64.0.15 │ │ Username: p0004_admin │ │ Password: aB3xK9mP │ │ 프론트엔드: https://pharmq.kr │ └─────────────────────────────────────────────────────────────┘ │ │ 사용자가 로그인 정보 복사 ↓ ┌─────────────────────────────────────────────────────────────┐ │ React 프론트엔드 (https://pharmq.kr) │ ├─────────────────────────────────────────────────────────────┤ │ 1. 로그인 페이지 │ │ 2. Username: p0004_admin 입력 │ │ 3. Password: aB3xK9mP 입력 │ │ 4. 로그인 버튼 클릭 │ └─────────────────┬───────────────────────────────────────────┘ │ │ POST /api/auth/login ↓ ┌─────────────────────────────────────────────────────────────┐ │ Gateway API (gateway.pharmq.kr:8000) │ ├─────────────────────────────────────────────────────────────┤ │ 1. gateway.db users 조회 │ │ 2. 비밀번호 검증 (pbkdf2_sha256) │ │ 3. JWT 토큰 생성 │ │ payload: { │ │ username: "p0004_admin", │ │ pharmacy_code: "P0004", │ │ role: "admin" │ │ } │ │ 4. 토큰 반환 │ └─────────────────┬───────────────────────────────────────────┘ │ │ JWT Token ↓ ┌─────────────────────────────────────────────────────────────┐ │ React 프론트엔드 │ ├─────────────────────────────────────────────────────────────┤ │ ✅ 로그인 성공! │ │ ✅ 대시보드 페이지 진입 │ │ ✅ API 요청 가능 (JWT 토큰 사용) │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 🎯 성공 기준 ### 기능적 요구사항 - ✅ 스크립트 실행 후 5분 이내 완료 - ✅ farmq.db에 약국 정보 자동 생성 - ✅ gateway.db에 사용자 계정 자동 생성 - ✅ 프론트엔드에서 즉시 로그인 가능 - ✅ API 통신 정상 작동 ### 비기능적 요구사항 - ✅ 에러 발생 시 롤백 - ✅ 중복 등록 방지 - ✅ 입력 검증 (전화번호 형식 등) - ✅ 로그 파일 생성 (추후) --- ## 📚 참고 자료 ### 관련 문서 - [GATEWAY_ARCHITECTURE_EXPLAINED.md](GATEWAY_ARCHITECTURE_EXPLAINED.md) - [DATABASE_MAPPING_EXPLAINED.md](DATABASE_MAPPING_EXPLAINED.md) ### 필요한 Python 라이브러리 ```bash # 대부분 기본 설치되어 있음 python3 -c "import sqlite3, secrets, string, argparse" # 기본 라이브러리 # passlib만 추가 설치 필요 pip3 install passlib ``` ### DB 경로 ```bash farmq.db: /srv/headscale-tailscale-replacement/farmq-admin/farmq.db gateway.db: /srv/pharmq-gateway/gateway.db ``` --- ## ✅ 체크리스트 ### 개발 - [ ] register_pharmacy.py 스크립트 작성 - [ ] headscale-quick-install.sh 수정 - [ ] 입력 검증 로직 추가 - [ ] 에러 핸들링 추가 ### 테스트 - [ ] 로컬 테스트 (farmq.db, gateway.db) - [ ] 새 VM에서 전체 플로우 테스트 - [ ] 프론트엔드 로그인 테스트 - [ ] API 통신 테스트 ### 문서화 - [ ] README 업데이트 - [ ] 주석 추가 - [ ] 트러블슈팅 가이드 작성 ### 배포 - [ ] Gitea에 커밋 - [ ] 기존 스크립트 백업 - [ ] 프로덕션 배포 --- **작성일:** 2025년 11월 14일 **작성자:** Claude Code **버전:** Headscale Auto-Register Plan v1.0