feat: 동물약 APC 이미지 지원 (CD_ITEM_UNIT_MEMBER 연동)
This commit is contained in:
272
backend/app.py
272
backend/app.py
@@ -23,9 +23,11 @@ from sqlalchemy import text
|
||||
from dotenv import load_dotenv
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# 환경 변수 로드
|
||||
load_dotenv()
|
||||
# 환경 변수 로드 (명시적 경로)
|
||||
env_path = Path(__file__).parent / '.env'
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
|
||||
# OpenAI import
|
||||
try:
|
||||
@@ -2627,6 +2629,268 @@ def admin_sales_detail():
|
||||
return render_template('admin_sales_detail.html')
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 동물약 추천 챗봇 API
|
||||
# =============================================================================
|
||||
|
||||
# 동물약 지식 베이스 (RAG 컨텍스트)
|
||||
ANIMAL_DRUG_KNOWLEDGE = """
|
||||
## 🐕 심장사상충 예방약 (매월 투여)
|
||||
- **하트가드 (Heartgard)**: 이버멕틴 성분, 츄어블, 소/중/대형견용, 고기맛
|
||||
- **다이로하트 (Dirohart)**: 이버멕틴 성분, 하트가드 제네릭, 경제적
|
||||
- **하트캅 (Heartcap)**: 밀베마이신 옥심, 태블릿, 정밀한 용량
|
||||
|
||||
## 🐕 외부기생충 (벼룩/진드기) 예방약
|
||||
- **넥스가드 (NexGard)**: 아폭솔라너 성분, 츄어블, 1개월 지속, 맛있는 소고기맛
|
||||
- **넥스가드 스펙트라**: 넥스가드 + 심장사상충 예방 (아폭솔라너 + 밀베마이신)
|
||||
- **브라벡토 (Bravecto)**: 플루랄라너 성분, 츄어블, **3개월** 지속 (12주)
|
||||
- **심파리카 (Simparica)**: 사롤라너 성분, 츄어블, 1개월 지속
|
||||
- **심파리카 트리오**: 심파리카 + 심장사상충 + 내부기생충 예방
|
||||
- **크레델리오 (Credelio)**: 로틸라너 성분, 1개월 지속, 소형 정제
|
||||
|
||||
## 🐱 고양이 전용
|
||||
- **브라벡토 스팟온 (고양이)**: 외부기생충, 3개월 지속, 바르는 타입
|
||||
- **레볼루션 (Revolution)**: 셀라멕틴 성분, 스팟온, 심장사상충 + 벼룩 + 귀진드기
|
||||
- **브로드라인**: 피프로닐 + 에피프리노미드, 내/외부기생충 + 심장사상충
|
||||
|
||||
## 🪱 내부기생충 (구충제)
|
||||
- **안텔민 (Antelmin)**: 프라지콴텔 + 피란텔, 회충/촌충/편충
|
||||
- **파라캅 (Paracap)**: 펜벤다졸 성분, 광범위 구충
|
||||
- **드론탈 (Drontal)**: 프라지콴텔 + 피란텔, 국제적 신뢰
|
||||
|
||||
## 💊 용량 안내
|
||||
- 체중별 제품 선택이 중요합니다
|
||||
- 소형견(5kg 미만), 중형견(5-10kg), 대형견(10-25kg), 초대형견(25kg 이상)
|
||||
- 고양이는 반드시 고양이 전용 제품 사용
|
||||
|
||||
## ⚠️ 주의사항
|
||||
- 콜리/셸티/보더콜리 등 MDR1 유전자 변이견은 이버멕틴 주의
|
||||
- 임신/수유 중인 동물은 수의사 상담 필요
|
||||
- 체중 정확히 측정 후 제품 선택
|
||||
"""
|
||||
|
||||
# 동물약 챗봇 System Prompt
|
||||
ANIMAL_CHAT_SYSTEM_PROMPT = """당신은 약국의 동물약 전문 상담사입니다. 반려동물 보호자에게 친절하고 전문적인 상담을 제공합니다.
|
||||
|
||||
**역할:**
|
||||
1. 동물약 추천 (심장사상충, 외부기생충, 구충제 등)
|
||||
2. 제품 비교 및 장단점 설명
|
||||
3. 용법/용량 안내
|
||||
4. 주의사항 안내
|
||||
|
||||
**대화 스타일:**
|
||||
- 친근하고 따뜻하게 🐕🐱
|
||||
- 이모지 적절히 활용
|
||||
- 간결하지만 정확한 정보
|
||||
- 최종 결정은 수의사 상담 권장
|
||||
|
||||
**제약:**
|
||||
- 처방전이 필요한 약은 수의사 처방 안내
|
||||
- 진단/치료는 수의사 영역임을 명시
|
||||
- 보유 제품 위주로 추천
|
||||
|
||||
**응답 형식:**
|
||||
- 짧고 명확하게 (3-5문장)
|
||||
- 추천 제품이 있으면 이름과 특징 언급
|
||||
- 체중/종류 확인 필요시 질문
|
||||
|
||||
{available_products}
|
||||
|
||||
{knowledge_base}"""
|
||||
|
||||
|
||||
def _get_animal_drugs():
|
||||
"""보유 중인 동물약 목록 조회 (APC 이미지 포함)"""
|
||||
try:
|
||||
drug_session = db_manager.get_session('PM_DRUG')
|
||||
# CD_ITEM_UNIT_MEMBER에서 APC 바코드 조회 (0230237로 시작하는 것)
|
||||
query = text("""
|
||||
SELECT
|
||||
G.DrugCode,
|
||||
G.GoodsName,
|
||||
G.Saleprice,
|
||||
G.BARCODE,
|
||||
(
|
||||
SELECT TOP 1 U.CD_CD_BARCODE
|
||||
FROM CD_ITEM_UNIT_MEMBER U
|
||||
WHERE U.DRUGCODE = G.DrugCode
|
||||
AND U.CD_CD_BARCODE LIKE '023%'
|
||||
ORDER BY U.CHANGE_DATE DESC
|
||||
) AS APC_CODE
|
||||
FROM CD_GOODS G
|
||||
WHERE G.POS_BOON = '010103'
|
||||
AND G.GoodsSelCode = 'B'
|
||||
ORDER BY G.GoodsName
|
||||
""")
|
||||
rows = drug_session.execute(query).fetchall()
|
||||
|
||||
result = []
|
||||
for r in rows:
|
||||
apc = r.APC_CODE if hasattr(r, 'APC_CODE') else None
|
||||
image_url = None
|
||||
if apc:
|
||||
# APC가 있으면 이미지 URL 생성
|
||||
image_url = f"https://ani.0bin.in/img/{apc}_F.jpg"
|
||||
|
||||
result.append({
|
||||
'code': r.DrugCode,
|
||||
'name': r.GoodsName,
|
||||
'price': float(r.Saleprice) if r.Saleprice else 0,
|
||||
'barcode': r.BARCODE or '',
|
||||
'apc': apc,
|
||||
'image_url': image_url
|
||||
})
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logging.warning(f"동물약 목록 조회 실패: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@app.route('/api/animal-chat', methods=['POST'])
|
||||
def api_animal_chat():
|
||||
"""
|
||||
동물약 추천 챗봇 API
|
||||
|
||||
Request:
|
||||
{
|
||||
"messages": [
|
||||
{"role": "user", "content": "강아지 심장사상충 약 추천해주세요"}
|
||||
]
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"message": "AI 응답 텍스트",
|
||||
"products": [{"name": "...", "price": ...}] // 언급된 보유 제품
|
||||
}
|
||||
"""
|
||||
try:
|
||||
if not OPENAI_AVAILABLE:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'AI 기능을 사용할 수 없습니다. 관리자에게 문의하세요.'
|
||||
}), 503
|
||||
|
||||
data = request.get_json()
|
||||
messages = data.get('messages', [])
|
||||
|
||||
if not messages:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '메시지를 입력해주세요.'
|
||||
}), 400
|
||||
|
||||
# 보유 동물약 목록 조회
|
||||
animal_drugs = _get_animal_drugs()
|
||||
available_products_text = ""
|
||||
if animal_drugs:
|
||||
product_list = "\n".join([
|
||||
f"- {d['name']} ({d['price']:,.0f}원)" for d in animal_drugs
|
||||
])
|
||||
available_products_text = f"""
|
||||
**현재 보유 동물약:**
|
||||
{product_list}
|
||||
"""
|
||||
|
||||
# System Prompt 구성
|
||||
system_prompt = ANIMAL_CHAT_SYSTEM_PROMPT.format(
|
||||
available_products=available_products_text,
|
||||
knowledge_base=ANIMAL_DRUG_KNOWLEDGE
|
||||
)
|
||||
|
||||
# OpenAI API 호출
|
||||
client = OpenAI(api_key=OPENAI_API_KEY)
|
||||
|
||||
api_messages = [{"role": "system", "content": system_prompt}]
|
||||
for msg in messages[-10:]: # 최근 10개 메시지만
|
||||
api_messages.append({
|
||||
"role": msg.get("role", "user"),
|
||||
"content": msg.get("content", "")
|
||||
})
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=OPENAI_MODEL,
|
||||
messages=api_messages,
|
||||
max_tokens=500,
|
||||
temperature=0.7
|
||||
)
|
||||
|
||||
ai_response = response.choices[0].message.content
|
||||
|
||||
# 응답에서 언급된 보유 제품 찾기 (부분 매칭)
|
||||
mentioned_products = []
|
||||
ai_response_lower = ai_response.lower()
|
||||
|
||||
for drug in animal_drugs:
|
||||
drug_name = drug['name']
|
||||
# 제품명에서 핵심 키워드 추출 (괄호 앞부분)
|
||||
# 예: "다이로하트정M(12~22kg)" → "다이로하트"
|
||||
base_name = drug_name.split('(')[0].split('/')[0].strip()
|
||||
# 사이즈 제거: "다이로하트정M" → "다이로하트"
|
||||
for suffix in ['정', '액', 'L', 'M', 'S', 'XL', 'XS', 'SS', 'mini']:
|
||||
if base_name.endswith(suffix):
|
||||
base_name = base_name[:-len(suffix)]
|
||||
base_name = base_name.strip()
|
||||
|
||||
# 핵심 키워드가 AI 응답에 포함되어 있는지 확인
|
||||
if len(base_name) >= 2 and base_name.lower() in ai_response_lower:
|
||||
# 중복 방지 (같은 제품 계열은 하나만)
|
||||
already_added = any(base_name.lower() in p['name'].lower() for p in mentioned_products)
|
||||
if not already_added:
|
||||
mentioned_products.append({
|
||||
'name': drug_name,
|
||||
'price': drug['price'],
|
||||
'code': drug['code'],
|
||||
'image_url': drug.get('image_url') # APC 이미지 URL
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': ai_response,
|
||||
'products': mentioned_products[:5], # 최대 5개
|
||||
'usage': {
|
||||
'input_tokens': response.usage.prompt_tokens,
|
||||
'output_tokens': response.usage.completion_tokens
|
||||
}
|
||||
})
|
||||
|
||||
except RateLimitError:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'AI 사용량 한도에 도달했습니다. 잠시 후 다시 시도해주세요.'
|
||||
}), 429
|
||||
except APITimeoutError:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'AI 응답 시간이 초과되었습니다. 다시 시도해주세요.'
|
||||
}), 504
|
||||
except Exception as e:
|
||||
logging.error(f"동물약 챗봇 오류: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류가 발생했습니다: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/animal-drugs')
|
||||
def api_animal_drugs():
|
||||
"""보유 동물약 목록 API"""
|
||||
try:
|
||||
drugs = _get_animal_drugs()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'items': drugs,
|
||||
'count': len(drugs)
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
# ===== 제품 검색 페이지 =====
|
||||
|
||||
@app.route('/admin/products')
|
||||
@@ -3660,5 +3924,5 @@ if __name__ == '__main__':
|
||||
else:
|
||||
logging.error(f"포트 {PORT} 해제 실패. 수동으로 확인하세요.")
|
||||
|
||||
# 개발 모드로 실행
|
||||
app.run(host='0.0.0.0', port=PORT, debug=True)
|
||||
# 프로덕션 모드로 실행
|
||||
app.run(host='0.0.0.0', port=PORT, debug=False, threaded=True)
|
||||
|
||||
Reference in New Issue
Block a user