추가된 문서:
- 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 <noreply@anthropic.com>
30 KiB
30 KiB
Headscale 자동 등록 및 DB 생성 개선 기획서
📅 작성일
2025년 11월 14일
🎯 목표
Headscale 설치 스크립트(headscale-quick-install.sh)를 개선하여:
- ✅ Headscale VPN 등록 (현재 완료)
- ✅ farmq.db 자동 생성 (신규)
- ✅ gateway.db 자동 생성 (신규)
- ✅ 즉시 프론트엔드 로그인 가능 (신규)
최종 결과: 스크립트 실행 → 즉시 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) │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
📝 사용자 입력 항목
필수 입력 항목
1. 약국 이름 (pharmacy_name)
예: "행복약국", "새서울약국"
2. 대표약사 이름 (owner_name)
예: "홍길동"
3. 대표약사 전화번호 (owner_phone)
예: "010-1234-5678"
4. 이메일 (owner_email) - 선택
예: "happy@pharmq.kr"
자동 생성 항목
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 (간단)
sqlite3 /srv/headscale-tailscale-replacement/farmq-admin/farmq.db \
"INSERT INTO pharmacies (pharmacy_code, pharmacy_name, ...) VALUES (...);"
장점:
- ✅ 추가 의존성 없음
- ✅ 스크립트에 바로 통합 가능
단점:
- ❌ 복잡한 쿼리 작성 어려움
- ❌ 에러 핸들링 제한적
Option B: Python 스크립트 (권장 ⭐)
python3 /srv/pharmq-gateway/scripts/register_pharmacy.py \
--name "행복약국" \
--owner "홍길동" \
--phone "010-1234-5678" \
--vpn-ip "100.64.0.15"
장점:
- ✅ 복잡한 로직 구현 가능
- ✅ 에러 핸들링 우수
- ✅ 검증 로직 추가 가능
- ✅ 비밀번호 해싱 자동
단점:
- ❌ Python 의존성 필요 (대부분 시스템에 기본 설치됨)
2. 스크립트 구조
# 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: 약국 정보 수집 (대화형)
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
#!/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 스크립트에서 호출
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
}
🎨 최종 사용자 경험
실행 예시
# 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 스크립트 생성 ⭐ (최우선)
/srv/pharmq-gateway/scripts/register_pharmacy.py
- farmq.db INSERT
- gateway.db users, pharmacy_members INSERT
- 비밀번호 해싱
- 에러 핸들링
Phase 2: Bash 스크립트 통합 ⭐
headscale-quick-install.sh 수정
- collect_pharmacy_info() 함수 추가
- register_to_farmq_and_gateway() 함수 추가
- show_login_info() 함수 추가
Phase 3: 테스트 ⭐
1. 새 VM에서 스크립트 실행
2. DB 확인
3. 프론트엔드 로그인 테스트
4. API 통신 테스트
Phase 4: 문서화
- 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 통신 정상 작동
비기능적 요구사항
- ✅ 에러 발생 시 롤백
- ✅ 중복 등록 방지
- ✅ 입력 검증 (전화번호 형식 등)
- ✅ 로그 파일 생성 (추후)
📚 참고 자료
관련 문서
필요한 Python 라이브러리
# 대부분 기본 설치되어 있음
python3 -c "import sqlite3, secrets, string, argparse" # 기본 라이브러리
# passlib만 추가 설치 필요
pip3 install passlib
DB 경로
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