docs: headscale 자동 등록 개선 계획 문서 추가
추가된 문서: - 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>
This commit is contained in:
538
FARMQ_ADMIN_INTEGRATION_ANALYSIS.md
Normal file
538
FARMQ_ADMIN_INTEGRATION_ANALYSIS.md
Normal file
@@ -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/<id> # 약국 정보 조회
|
||||
PUT /api/pharmacy/<id> # 약국 정보 수정
|
||||
DELETE /api/pharmacy/<id> # 약국 삭제
|
||||
```
|
||||
|
||||
### 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 <<EOF
|
||||
{
|
||||
"pharmacy_name": "$PHARMACY_NAME",
|
||||
"owner_name": "$OWNER_NAME",
|
||||
"owner_phone": "$OWNER_PHONE",
|
||||
"owner_email": "$OWNER_EMAIL",
|
||||
"phone": "$PHARMACY_PHONE",
|
||||
"address": "$PHARMACY_ADDRESS",
|
||||
"api_port": 8082
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# API 호출
|
||||
RESPONSE=$(curl -s -X POST \
|
||||
http://localhost:5001/api/pharmacy \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_DATA")
|
||||
|
||||
# 응답 확인
|
||||
SUCCESS=$(echo "$RESPONSE" | grep -o '"success"[[:space:]]*:[[:space:]]*true')
|
||||
|
||||
if [ -n "$SUCCESS" ]; then
|
||||
# pharmacy_code 추출
|
||||
PHARMACY_CODE=$(echo "$RESPONSE" | grep -o '"pharmacy_code"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
print_success "약국 등록 완료! 코드: $PHARMACY_CODE"
|
||||
|
||||
# VPN IP 업데이트 (별도 API 호출 필요)
|
||||
update_pharmacy_vpn_ip "$PHARMACY_CODE" "$TAILSCALE_IP"
|
||||
|
||||
return 0
|
||||
else
|
||||
print_error "약국 등록 실패"
|
||||
echo "$RESPONSE"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
#### VPN IP 업데이트
|
||||
```bash
|
||||
update_pharmacy_vpn_ip() {
|
||||
local PHARMACY_CODE=$1
|
||||
local VPN_IP=$2
|
||||
|
||||
# pharmacy_id 조회 필요 (또는 UPDATE API 수정)
|
||||
# 현재는 PUT /api/pharmacy/<id>만 있음
|
||||
|
||||
# 임시 방안: 직접 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/<pharmacy_code>/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/<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/<code>/vpn-ip` - VPN IP 업데이트
|
||||
2. `GET /api/pharmacy/code/<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
|
||||
Reference in New Issue
Block a user