From 60be9daff433062a96678c7c73c11e91d190a967 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Nov 2025 09:40:54 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20headscale=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EA=B0=9C=EC=84=A0=20=EA=B3=84=ED=9A=8D=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 추가된 문서: - SCRIPT_IMPROVEMENT_PLAN.md: 스크립트 개선 계획 - FARMQ_ADMIN_INTEGRATION_ANALYSIS.md: farmq-admin API 분석 - HEADSCALE_AUTO_REGISTER_PLAN.md: 초기 계획 주요 내용: - Headscale VPN 등록 시 자동 DB 생성 - API 엔드포인트: demo.pharmq.kr, gateway.pharmq.kr 🤖 Generated with Claude Code Co-Authored-By: Claude --- FARMQ_ADMIN_INTEGRATION_ANALYSIS.md | 538 ++++++++++++++++++ HEADSCALE_AUTO_REGISTER_PLAN.md | 838 ++++++++++++++++++++++++++++ SCRIPT_IMPROVEMENT_PLAN.md | 263 +++++++++ 3 files changed, 1639 insertions(+) create mode 100644 FARMQ_ADMIN_INTEGRATION_ANALYSIS.md create mode 100644 HEADSCALE_AUTO_REGISTER_PLAN.md create mode 100644 SCRIPT_IMPROVEMENT_PLAN.md diff --git a/FARMQ_ADMIN_INTEGRATION_ANALYSIS.md b/FARMQ_ADMIN_INTEGRATION_ANALYSIS.md new file mode 100644 index 0000000..6660c06 --- /dev/null +++ b/FARMQ_ADMIN_INTEGRATION_ANALYSIS.md @@ -0,0 +1,538 @@ +# farmq-admin 통합 분석 및 개선 방안 + +## 📅 작성일 +**2025년 11월 14일** + +--- + +## 🎯 발견 사항 + +### farmq-admin이 이미 존재하고 실행 중! + +```bash +프로세스: /srv/headscale-tailscale-replacement/farmq-admin/venv/bin/python app.py +포트: 5001 +상태: 실행 중 (Nov 05부터 계속 실행) +``` + +**중요:** farmq-admin은 Flask 기반 웹 애플리케이션으로, **farmq.db를 관리하는 API를 이미 제공**하고 있습니다! + +--- + +## 📊 farmq-admin 구조 분석 + +### 1. 실행 정보 +```bash +위치: /srv/headscale-tailscale-replacement/farmq-admin/ +메인: app.py (Flask 애플리케이션) +포트: 5001 +DB: farmq.db (SQLAlchemy ORM 사용) +``` + +### 2. 주요 API 엔드포인트 + +#### 약국 관리 API +```python +POST /api/pharmacy # 새 약국 생성 ✨ +GET /api/pharmacy/ # 약국 정보 조회 +PUT /api/pharmacy/ # 약국 정보 수정 +DELETE /api/pharmacy/ # 약국 삭제 +``` + +### 3. 약국 생성 API 상세 (`POST /api/pharmacy`) + +**요청 예시:** +```json +POST http://localhost:5001/api/pharmacy +Content-Type: application/json + +{ + "pharmacy_name": "행복약국", + "owner_name": "홍길동", + "owner_phone": "010-1234-5678", + "owner_email": "happy@pharmq.kr", + "phone": "02-1234-5678", + "address": "서울시 강남구...", + "api_port": 8082 +} +``` + +**응답 예시:** +```json +{ + "success": true, + "message": "약국 \"행복약국\" (코드: P004) 생성 완료", + "pharmacy": { + "id": 4, + "pharmacy_code": "P004", + "pharmacy_name": "행복약국", + "tailscale_ip": null, + "api_port": 8082, + "status": "active", + "owner_name": "홍길동", + "owner_phone": "010-1234-5678", + "owner_email": "happy@pharmq.kr" + } +} +``` + +### 4. 자동 생성 로직 (이미 구현됨!) + +```python +# app.py Line 418-433 +# pharmacy_code 자동 생성 (P001~P999) +last_pharmacy = farmq_session.query(PharmacyInfo)\ + .filter(PharmacyInfo.pharmacy_code.like('P%'))\ + .order_by(PharmacyInfo.pharmacy_code.desc())\ + .first() + +if last_pharmacy and last_pharmacy.pharmacy_code: + try: + last_num = int(last_pharmacy.pharmacy_code[1:]) + new_num = last_num + 1 + except: + new_num = 1 +else: + new_num = 1 + +pharmacy_code = f"P{new_num:03d}" # P001, P002, P003... +``` + +**결과:** 마지막 약국 코드를 찾아서 자동으로 +1 증가! + +--- + +## 🔄 개선된 통합 방안 + +### 기존 계획 vs 새로운 발견 + +#### ❌ 기존 계획 (불필요) +``` +Python 스크립트로 직접 farmq.db INSERT +→ SQL 쿼리 작성 +→ 에러 핸들링 직접 구현 +→ 검증 로직 직접 구현 +``` + +#### ✅ 새로운 방안 (API 활용) +``` +farmq-admin API 호출 +→ 이미 모든 로직 구현됨 +→ 검증, 에러 핸들링 완료 +→ pharmacy_code 자동 생성 +``` + +--- + +## 🚀 최종 개선 방안 + +### 방법 1: farmq-admin API 활용 (권장 ⭐) + +#### 장점 +- ✅ 이미 구현된 API 활용 +- ✅ pharmacy_code 자동 생성 +- ✅ 검증 로직 포함 +- ✅ 에러 핸들링 완료 +- ✅ 유지보수 용이 + +#### 구현 +```bash +# headscale-quick-install.sh에서 호출 +register_to_farmq_api() { + print_status "farmq-admin API를 통해 약국 등록 중..." + + # JSON 데이터 생성 + JSON_DATA=$(cat <만 있음 + + # 임시 방안: 직접 DB 업데이트 + python3 << EOF +import sqlite3 +conn = sqlite3.connect('/srv/headscale-tailscale-replacement/farmq-admin/farmq.db') +cursor = conn.cursor() +cursor.execute(""" + UPDATE pharmacies + SET tailscale_ip = ? + WHERE pharmacy_code = ? +""", ("$VPN_IP", "$PHARMACY_CODE")) +conn.commit() +conn.close() +print("VPN IP 업데이트 완료: $PHARMACY_CODE → $VPN_IP") +EOF +} +``` + +### 방법 2: 하이브리드 방식 (대안) + +```bash +register_pharmacy_hybrid() { + # 1. farmq-admin API로 기본 정보 등록 + PHARMACY_CODE=$(call_farmq_api_create) + + # 2. VPN IP는 직접 업데이트 (API에 없는 필드) + update_vpn_ip_direct "$PHARMACY_CODE" "$TAILSCALE_IP" + + # 3. gateway.db는 Python 스크립트로 생성 + create_gateway_user "$PHARMACY_CODE" +} +``` + +--- + +## 📋 수정된 전체 흐름 + +### 새로운 스크립트 흐름 +``` +1. OS 감지 +2. Tailscale 설치 +3. Tailscale 서비스 시작 +4. Headscale 등록 (preauth key 사용) +5. VPN IP 할당 받음 (예: 100.64.0.15) +6. 연결 확인 +7. ✨ 약국 정보 입력 받기 (대화형) +8. ✨ farmq-admin API 호출 → farmq.db 등록 + - pharmacy_code 자동 생성 (API가 처리) + - 기본 정보 저장 +9. ✨ VPN IP 업데이트 (직접 DB 또는 API 개선) +10. ✨ gateway.db에 사용자 생성 (Python 스크립트) +11. ✨ 로그인 정보 출력 +12. 종료 ✅ +``` + +--- + +## 🛠️ 필요한 개선 사항 + +### farmq-admin API 개선 (선택사항) + +#### 1. VPN IP 업데이트 API 추가 +```python +# app.py에 추가 +@app.route('/api/pharmacy//vpn-ip', methods=['PUT']) +def api_update_pharmacy_vpn_ip(pharmacy_code): + """약국 VPN IP 업데이트""" + try: + data = request.get_json() + vpn_ip = data.get('vpn_ip', '').strip() + + if not vpn_ip: + return jsonify({ + 'success': False, + 'error': 'VPN IP는 필수입니다.' + }), 400 + + farmq_session = get_farmq_session() + try: + pharmacy = farmq_session.query(PharmacyInfo).filter( + PharmacyInfo.pharmacy_code == pharmacy_code + ).first() + + if not pharmacy: + return jsonify({ + 'success': False, + 'error': '약국을 찾을 수 없습니다.' + }), 404 + + pharmacy.tailscale_ip = vpn_ip + farmq_session.commit() + + return jsonify({ + 'success': True, + 'message': f'VPN IP 업데이트 완료: {pharmacy_code} → {vpn_ip}' + }) + + finally: + farmq_session.close() + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'서버 오류: {str(e)}' + }), 500 +``` + +**사용:** +```bash +curl -X PUT http://localhost:5001/api/pharmacy/P004/vpn-ip \ + -H "Content-Type: application/json" \ + -d '{"vpn_ip": "100.64.0.15"}' +``` + +#### 2. pharmacy_code로 조회 API 추가 +```python +@app.route('/api/pharmacy/code/', methods=['GET']) +def api_get_pharmacy_by_code(pharmacy_code): + """약국 코드로 약국 정보 조회""" + try: + farmq_session = get_farmq_session() + try: + pharmacy = farmq_session.query(PharmacyInfo).filter( + PharmacyInfo.pharmacy_code == pharmacy_code + ).first() + + if not pharmacy: + return jsonify({ + 'success': False, + 'error': '약국을 찾을 수 없습니다.' + }), 404 + + return jsonify({ + 'success': True, + 'pharmacy': pharmacy.to_dict() + }) + + finally: + farmq_session.close() + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'서버 오류: {str(e)}' + }), 500 +``` + +--- + +## 📊 비교표 + +| 항목 | 직접 DB 접근 | farmq-admin API | +|------|-------------|-----------------| +| 구현 난이도 | 중 | 쉬움 ⭐ | +| pharmacy_code 생성 | 직접 구현 필요 | 자동 처리 ✅ | +| 검증 로직 | 직접 구현 필요 | 이미 구현됨 ✅ | +| 에러 핸들링 | 직접 구현 필요 | 이미 구현됨 ✅ | +| 유지보수 | 어려움 | 쉬움 ✅ | +| VPN IP 업데이트 | 직접 가능 | API 개선 필요 | +| 의존성 | SQLite3만 | curl 필요 | + +--- + +## 🎯 최종 권장 사항 + +### Phase 1: 최소 변경 (즉시 적용 가능) + +```bash +# headscale-quick-install.sh 수정 + +register_pharmacy() { + print_status "약국 등록 중..." + + # 1. farmq-admin API로 약국 생성 + RESPONSE=$(curl -s -X POST \ + http://localhost:5001/api/pharmacy \ + -H "Content-Type: application/json" \ + -d "{ + \"pharmacy_name\": \"$PHARMACY_NAME\", + \"owner_name\": \"$OWNER_NAME\", + \"owner_phone\": \"$OWNER_PHONE\", + \"owner_email\": \"$OWNER_EMAIL\", + \"api_port\": 8082 + }") + + # 2. pharmacy_code 추출 + PHARMACY_CODE=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('pharmacy',{}).get('pharmacy_code',''))") + + # 3. VPN IP 업데이트 (직접 DB) + python3 << EOF +import sqlite3 +conn = sqlite3.connect('/srv/headscale-tailscale-replacement/farmq-admin/farmq.db') +cursor = conn.cursor() +cursor.execute("UPDATE pharmacies SET tailscale_ip = ? WHERE pharmacy_code = ?", + ("$TAILSCALE_IP", "$PHARMACY_CODE")) +conn.commit() +conn.close() +EOF + + # 4. gateway.db 사용자 생성 (별도 스크립트) + python3 /srv/pharmq-gateway/scripts/create_gateway_user.py \ + --pharmacy-code "$PHARMACY_CODE" \ + --owner "$OWNER_NAME" \ + --phone "$OWNER_PHONE" \ + --email "$OWNER_EMAIL" +} +``` + +### Phase 2: farmq-admin API 개선 (선택) + +app.py에 다음 추가: +1. `PUT /api/pharmacy//vpn-ip` - VPN IP 업데이트 +2. `GET /api/pharmacy/code/` - 코드로 조회 + +### Phase 3: 완전 통합 + +gateway.db 생성도 API로 통합 (gateway API 서버에서) + +--- + +## 📝 구현 예시 (최소 변경) + +### 1. gateway 사용자 생성 스크립트 + +**파일:** `/srv/pharmq-gateway/scripts/create_gateway_user.py` + +```python +#!/usr/bin/env python3 +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 create_gateway_user(args): + username = f"{args.pharmacy_code.lower()}_admin" + password = generate_password(8) + password_hash = pbkdf2_sha256.hash(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, + password_hash, + args.owner, + args.phone, + args.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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + args.pharmacy_code, user_id, '대표약사', 'owner', + True, True, True, True, + datetime.now().isoformat(), + datetime.now().isoformat() + )) + + conn.commit() + + # 결과 출력 (JSON) + import json + print(json.dumps({ + 'success': True, + 'username': username, + 'password': password, + 'pharmacy_code': args.pharmacy_code + })) + + except Exception as e: + import json + print(json.dumps({ + 'success': False, + 'error': str(e) + })) + sys.exit(1) + finally: + conn.close() + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--pharmacy-code', required=True) + parser.add_argument('--owner', required=True) + parser.add_argument('--phone', required=True) + parser.add_argument('--email', required=True) + parser.add_argument('--gateway-db', + default='/srv/pharmq-gateway/gateway.db') + + args = parser.parse_args() + create_gateway_user(args) +``` + +--- + +## ✅ 결론 + +### 핵심 발견 +**farmq-admin이 이미 약국 생성 API를 제공하고 있음!** +- pharmacy_code 자동 생성 ✅ +- 검증 로직 완료 ✅ +- 에러 핸들링 완료 ✅ + +### 개선 방안 +1. **farmq.db**: farmq-admin API 활용 (POST /api/pharmacy) +2. **VPN IP**: 직접 DB 업데이트 (또는 API 추가) +3. **gateway.db**: Python 스크립트로 생성 + +### 장점 +- ✅ 기존 시스템 재사용 +- ✅ 중복 코드 방지 +- ✅ 유지보수 용이 +- ✅ 빠른 구현 가능 + +--- + +**작성일:** 2025년 11월 14일 +**작성자:** Claude Code +**버전:** farmq-admin Integration Analysis v1.0 diff --git a/HEADSCALE_AUTO_REGISTER_PLAN.md b/HEADSCALE_AUTO_REGISTER_PLAN.md new file mode 100644 index 0000000..a156202 --- /dev/null +++ b/HEADSCALE_AUTO_REGISTER_PLAN.md @@ -0,0 +1,838 @@ +# 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 diff --git a/SCRIPT_IMPROVEMENT_PLAN.md b/SCRIPT_IMPROVEMENT_PLAN.md new file mode 100644 index 0000000..4e61a69 --- /dev/null +++ b/SCRIPT_IMPROVEMENT_PLAN.md @@ -0,0 +1,263 @@ +# headscale-quick-install.sh 개선 계획 + +## 목표 +Headscale VPN 등록 시 **farmq.db와 gateway.db에 자동으로 약국 및 관리자 계정 생성**하여 +스크립트 실행만으로 **즉시 프론트엔드 로그인 가능**하게 만들기 + +## 자동 생성 플로우 + +``` +1. Headscale VPN 등록 → VPN IP 부여 (예: 100.64.0.25) +2. farmq-admin API 호출 → farmq.db에 약국 생성 + - pharmacy_code: P0005 (자동 증가) + - pharmacy_name: 사용자 입력 + - tailscale_ip: 100.64.0.25 (VPN IP) + - hira_code: 사용자 입력 (선택) + - api_port: 8082 (기본값) + +3. gateway API 호출 → gateway.db에 admin 계정 생성 + - username: p0005 (pharmacy_code 소문자) + - password: 1234 (기본 비밀번호) + - email: p0005@pharmq.internal + - name: {pharmacy_name} 관리자 + - role: admin + - primary_pharmacy_code: P0005 + - pharmacy_members에도 자동 매핑됨 + +4. 로그인 정보 출력 +``` + +## 추가할 함수들 + +### 1. `collect_pharmacy_info()` +약국 기본 정보를 사용자로부터 입력받음 + +```bash +collect_pharmacy_info() { + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${WHITE}약국 정보 입력${NC}" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + # 약국명 입력 (필수) + while [ -z "$PHARMACY_NAME" ]; do + read -p "약국명을 입력하세요: " PHARMACY_NAME + done + + # 요양기관부호 입력 (선택) + read -p "요양기관부호 (선택, Enter로 건너뛰기): " HIRA_CODE + + # 약국 주소 입력 (선택) + read -p "약국 주소 (선택): " PHARMACY_ADDRESS + + # 약국장 이름 입력 (선택) + read -p "약국장 이름 (선택): " OWNER_NAME + + # 연락처 입력 (선택) + read -p "약국 연락처 (선택): " PHARMACY_PHONE + + echo -e "${GREEN}✓ 약국 정보 입력 완료${NC}" +} +``` + +### 2. `get_assigned_vpn_ip()` +Headscale에서 부여받은 VPN IP 가져오기 + +```bash +get_assigned_vpn_ip() { + echo -e "${BLUE}VPN IP 확인 중...${NC}" + + # tailscale status로 IP 추출 + VPN_IP=$(tailscale status --json 2>/dev/null | grep -oP '"TailscaleIPs":\["(\d+\.\d+\.\d+\.\d+)"' | grep -oP '\d+\.\d+\.\d+\.\d+' | head -1) + + if [ -z "$VPN_IP" ]; then + echo -e "${RED}✗ VPN IP를 가져올 수 없습니다${NC}" + return 1 + fi + + echo -e "${GREEN}✓ VPN IP: $VPN_IP${NC}" + return 0 +} +``` + +### 3. `create_pharmacy_via_api()` +farmq-admin API를 호출하여 약국 생성 + +```bash +create_pharmacy_via_api() { + echo -e "${BLUE}약국 등록 중 (farmq.db)...${NC}" + + # JSON 데이터 구성 + JSON_DATA=$(cat <