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:
parent
7a793ea77d
commit
60be9daff4
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
|
||||||
838
HEADSCALE_AUTO_REGISTER_PLAN.md
Normal file
838
HEADSCALE_AUTO_REGISTER_PLAN.md
Normal file
@ -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
|
||||||
263
SCRIPT_IMPROVEMENT_PLAN.md
Normal file
263
SCRIPT_IMPROVEMENT_PLAN.md
Normal file
@ -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 <<EOF
|
||||||
|
{
|
||||||
|
"pharmacy_name": "$PHARMACY_NAME",
|
||||||
|
"vpn_ip": "$VPN_IP",
|
||||||
|
"hira_code": "$HIRA_CODE",
|
||||||
|
"address": "$PHARMACY_ADDRESS",
|
||||||
|
"owner_name": "$OWNER_NAME",
|
||||||
|
"phone": "$PHARMACY_PHONE",
|
||||||
|
"api_port": 8082
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# API 호출 (외부 도메인)
|
||||||
|
RESPONSE=$(curl -s -X POST https://demo.pharmq.kr/api/pharmacy \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$JSON_DATA")
|
||||||
|
|
||||||
|
# pharmacy_code 추출
|
||||||
|
PHARMACY_CODE=$(echo "$RESPONSE" | grep -oP '"pharmacy_code":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
|
||||||
|
if [ -z "$PHARMACY_CODE" ]; then
|
||||||
|
echo -e "${RED}✗ 약국 생성 실패${NC}"
|
||||||
|
echo "$RESPONSE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ 약국 생성 완료: $PHARMACY_CODE${NC}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. `create_gateway_user_via_api()`
|
||||||
|
gateway API를 호출하여 관리자 계정 생성
|
||||||
|
|
||||||
|
```bash
|
||||||
|
create_gateway_user_via_api() {
|
||||||
|
echo -e "${BLUE}관리자 계정 생성 중 (gateway.db)...${NC}"
|
||||||
|
|
||||||
|
# username: pharmacy_code 소문자 (P0005 → p0005)
|
||||||
|
USERNAME=$(echo "$PHARMACY_CODE" | tr '[:upper:]' '[:lower:]')
|
||||||
|
PASSWORD="1234" # 기본 비밀번호
|
||||||
|
EMAIL="${USERNAME}@pharmq.internal"
|
||||||
|
|
||||||
|
# JSON 데이터 구성
|
||||||
|
JSON_DATA=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"username": "$USERNAME",
|
||||||
|
"email": "$EMAIL",
|
||||||
|
"password": "$PASSWORD",
|
||||||
|
"name": "${PHARMACY_NAME} 관리자",
|
||||||
|
"phone": "$PHARMACY_PHONE",
|
||||||
|
"primary_pharmacy_code": "$PHARMACY_CODE",
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# API 호출 (외부 도메인)
|
||||||
|
RESPONSE=$(curl -s -X POST https://gateway.pharmq.kr/api/auth/register \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$JSON_DATA")
|
||||||
|
|
||||||
|
# 성공 여부 확인
|
||||||
|
if echo "$RESPONSE" | grep -q '"success":true'; then
|
||||||
|
echo -e "${GREEN}✓ 관리자 계정 생성 완료${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ 관리자 계정 생성 실패${NC}"
|
||||||
|
echo "$RESPONSE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. `display_login_credentials()`
|
||||||
|
생성된 로그인 정보 출력
|
||||||
|
|
||||||
|
```bash
|
||||||
|
display_login_credentials() {
|
||||||
|
echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo -e "${WHITE}🎉 설치 및 등록 완료!${NC}"
|
||||||
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}약국 정보:${NC}"
|
||||||
|
echo -e " 약국 코드: ${WHITE}$PHARMACY_CODE${NC}"
|
||||||
|
echo -e " 약국명: ${WHITE}$PHARMACY_NAME${NC}"
|
||||||
|
echo -e " VPN IP: ${WHITE}$VPN_IP${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}프론트엔드 로그인 정보:${NC}"
|
||||||
|
echo -e " URL: ${WHITE}https://pharmq.kr${NC}"
|
||||||
|
echo -e " 아이디: ${WHITE}$(echo "$PHARMACY_CODE" | tr '[:upper:]' '[:lower:]')${NC}"
|
||||||
|
echo -e " 비밀번호: ${WHITE}1234${NC}"
|
||||||
|
echo -e " ${YELLOW}⚠ 최초 로그인 후 비밀번호를 변경하세요!${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## main() 함수 수정
|
||||||
|
|
||||||
|
```bash
|
||||||
|
main() {
|
||||||
|
print_header "팜큐(FARMQ) Headscale 원클릭 설치"
|
||||||
|
|
||||||
|
# 사전 체크
|
||||||
|
detect_os
|
||||||
|
check_requirements
|
||||||
|
|
||||||
|
# 설치 과정
|
||||||
|
install_tailscale
|
||||||
|
start_tailscale
|
||||||
|
register_headscale
|
||||||
|
|
||||||
|
# VPN IP 확인
|
||||||
|
sleep 3 # Headscale에서 IP 할당 대기
|
||||||
|
get_assigned_vpn_ip || exit 1
|
||||||
|
|
||||||
|
# 약국 정보 수집
|
||||||
|
collect_pharmacy_info
|
||||||
|
|
||||||
|
# 약국 및 계정 생성
|
||||||
|
create_pharmacy_via_api || exit 1
|
||||||
|
create_gateway_user_via_api || exit 1
|
||||||
|
|
||||||
|
# 사후 설정
|
||||||
|
configure_firewall
|
||||||
|
verify_connection
|
||||||
|
|
||||||
|
# 정리 및 완료
|
||||||
|
cleanup
|
||||||
|
display_login_credentials
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 엔드포인트
|
||||||
|
|
||||||
|
| API | URL | 용도 | 접근 방식 |
|
||||||
|
|-----|-----|------|----------|
|
||||||
|
| farmq-admin | https://demo.pharmq.kr/api/pharmacy | 약국 생성 | 외부 도메인 (HTTPS) |
|
||||||
|
| gateway | https://gateway.pharmq.kr/api/auth/register | 사용자 생성 | 외부 도메인 (HTTPS) |
|
||||||
|
|
||||||
|
**장점**:
|
||||||
|
- ✅ 모든 API가 외부 도메인으로 통일되어 있음
|
||||||
|
- ✅ HTTPS로 보안 통신
|
||||||
|
- ✅ 내부 네트워크 접근 불필요
|
||||||
|
- ✅ VPN 망 내부/외부 어디서든 실행 가능
|
||||||
|
|
||||||
|
## 보안 고려사항
|
||||||
|
|
||||||
|
1. **기본 비밀번호 1234**: 간단하지만 프론트엔드에서 강제 변경 유도 필요
|
||||||
|
2. **API 키 인증**: 현재는 public API지만 나중에 인증 토큰 추가 고려
|
||||||
|
3. **HTTPS**: 현재 HTTP이지만 production에서는 HTTPS 사용 권장
|
||||||
|
|
||||||
|
## 에러 처리
|
||||||
|
|
||||||
|
- VPN IP 할당 실패 시 스크립트 중단
|
||||||
|
- 약국 생성 실패 시 스크립트 중단
|
||||||
|
- 사용자 생성 실패 시 경고만 출력 (약국은 이미 생성됨)
|
||||||
|
|
||||||
|
## 테스트 시나리오
|
||||||
|
|
||||||
|
1. ✅ 정상 플로우: 모든 정보 입력 → 약국 및 계정 생성 → 로그인 성공
|
||||||
|
2. ✅ 선택 정보 생략: 약국명만 입력 → 약국 및 계정 생성 → 로그인 성공
|
||||||
|
3. ✅ VPN IP 할당 실패: 에러 메시지 출력 후 종료
|
||||||
|
4. ✅ 약국 생성 실패: 에러 메시지 출력 후 종료
|
||||||
|
5. ✅ 사용자 생성 실패: 경고 출력, 수동 생성 안내
|
||||||
|
|
||||||
|
## 다음 단계
|
||||||
|
|
||||||
|
1. ✅ farmq-admin API 개선 (VPN IP, hira_code 지원) - 완료
|
||||||
|
2. ✅ gateway API 개선 (pharmacy 검증, pharmacy_members 자동 추가) - 완료
|
||||||
|
3. ⏳ headscale-quick-install.sh 수정 - 진행 중
|
||||||
|
4. ⏳ 통합 테스트
|
||||||
Loading…
Reference in New Issue
Block a user