feat: 동물약 APC 이미지 지원 (CD_ITEM_UNIT_MEMBER 연동)
This commit is contained in:
parent
dd28958a59
commit
b95e14419e
272
backend/app.py
272
backend/app.py
@ -23,9 +23,11 @@ from sqlalchemy import text
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
# 환경 변수 로드
|
# 환경 변수 로드 (명시적 경로)
|
||||||
load_dotenv()
|
env_path = Path(__file__).parent / '.env'
|
||||||
|
load_dotenv(dotenv_path=env_path)
|
||||||
|
|
||||||
# OpenAI import
|
# OpenAI import
|
||||||
try:
|
try:
|
||||||
@ -2627,6 +2629,268 @@ def admin_sales_detail():
|
|||||||
return render_template('admin_sales_detail.html')
|
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')
|
@app.route('/admin/products')
|
||||||
@ -3660,5 +3924,5 @@ if __name__ == '__main__':
|
|||||||
else:
|
else:
|
||||||
logging.error(f"포트 {PORT} 해제 실패. 수동으로 확인하세요.")
|
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)
|
||||||
|
|||||||
@ -48,11 +48,224 @@
|
|||||||
|
|
||||||
/* ── 컨텐츠 ── */
|
/* ── 컨텐츠 ── */
|
||||||
.content {
|
.content {
|
||||||
max-width: 1200px;
|
max-width: 1100px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 24px 20px 60px;
|
padding: 24px 20px 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 플로팅 챗봇 ── */
|
||||||
|
.chatbot-panel {
|
||||||
|
position: fixed;
|
||||||
|
right: 24px;
|
||||||
|
bottom: 90px;
|
||||||
|
width: 370px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
box-shadow: 0 8px 40px rgba(0,0,0,0.15);
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 520px;
|
||||||
|
max-height: calc(100vh - 120px);
|
||||||
|
z-index: 998;
|
||||||
|
animation: chatSlideUp 0.3s ease;
|
||||||
|
}
|
||||||
|
.chatbot-panel.open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
@keyframes chatSlideUp {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.chatbot-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.chatbot-header h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.chatbot-header p {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.85;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.chatbot-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.chat-message {
|
||||||
|
max-width: 85%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.chat-message.user {
|
||||||
|
background: #8b5cf6;
|
||||||
|
color: #fff;
|
||||||
|
align-self: flex-end;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.chat-message.assistant {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #334155;
|
||||||
|
align-self: flex-start;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
.chat-message.system {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
.chat-message .products-mentioned {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px dashed #cbd5e1;
|
||||||
|
}
|
||||||
|
.chat-message .product-chip {
|
||||||
|
display: inline-block;
|
||||||
|
background: #10b981;
|
||||||
|
color: #fff;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 2px 4px 2px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.chat-message .product-chip:hover {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
|
.chatbot-input {
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.chatbot-input input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.chatbot-input input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #10b981;
|
||||||
|
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.1);
|
||||||
|
}
|
||||||
|
.chatbot-input button {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #10b981;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.chatbot-input button:hover { background: #059669; }
|
||||||
|
.chatbot-input button:disabled { background: #94a3b8; cursor: not-allowed; }
|
||||||
|
.chatbot-suggestions {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.suggestion-chip {
|
||||||
|
background: #e0e7ff;
|
||||||
|
color: #4338ca;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.suggestion-chip:hover {
|
||||||
|
background: #c7d2fe;
|
||||||
|
}
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
.typing-indicator span {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #94a3b8;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: typing 1.4s infinite;
|
||||||
|
}
|
||||||
|
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
@keyframes typing {
|
||||||
|
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
|
||||||
|
30% { transform: translateY(-6px); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 챗봇 토글 버튼 (항상 표시) ── */
|
||||||
|
.chatbot-toggle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.4);
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.chatbot-toggle:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 6px 28px rgba(16, 185, 129, 0.5);
|
||||||
|
}
|
||||||
|
.chatbot-toggle.active {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
box-shadow: 0 4px 20px rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모바일 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.chatbot-panel {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 75vh;
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
}
|
||||||
|
.chatbot-toggle { bottom: 16px; right: 16px; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── 검색 영역 ── */
|
/* ── 검색 영역 ── */
|
||||||
.search-section {
|
.search-section {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@ -344,7 +557,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1>🔍 제품 검색</h1>
|
<h1>🔍 제품 검색</h1>
|
||||||
<p>전체 제품 검색 · QR 라벨 인쇄</p>
|
<p>전체 제품 검색 · QR 라벨 인쇄 · 🐾 동물약 AI 상담</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@ -395,8 +608,36 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div><!-- /.content -->
|
||||||
|
|
||||||
|
<!-- 동물약 챗봇 패널 -->
|
||||||
|
<div class="chatbot-panel" id="chatbotPanel">
|
||||||
|
<div class="chatbot-header">
|
||||||
|
<h2>🐾 동물약 AI 상담</h2>
|
||||||
|
<p>심장사상충, 외부기생충, 구충제 등 무엇이든 물어보세요</p>
|
||||||
|
</div>
|
||||||
|
<div class="chatbot-suggestions">
|
||||||
|
<button class="suggestion-chip" onclick="sendSuggestion('강아지 심장사상충 약 추천해줘')">🐕 심장사상충약</button>
|
||||||
|
<button class="suggestion-chip" onclick="sendSuggestion('넥스가드랑 브라벡토 차이점')">넥스가드 vs 브라벡토</button>
|
||||||
|
<button class="suggestion-chip" onclick="sendSuggestion('고양이 벼룩 약 뭐가 좋아?')">🐱 고양이 벼룩약</button>
|
||||||
|
<button class="suggestion-chip" onclick="sendSuggestion('강아지 구충제 추천')">🪱 구충제</button>
|
||||||
|
</div>
|
||||||
|
<div class="chatbot-messages" id="chatMessages">
|
||||||
|
<div class="chat-message assistant">
|
||||||
|
안녕하세요! 🐾 동물약 상담 AI입니다.<br><br>
|
||||||
|
반려동물의 <strong>심장사상충 예방</strong>, <strong>벼룩/진드기 예방</strong>, <strong>구충제</strong> 등에 대해 무엇이든 물어보세요!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chatbot-input">
|
||||||
|
<input type="text" id="chatInput" placeholder="예: 5kg 강아지 심장사상충 약 추천해줘"
|
||||||
|
onkeypress="if(event.key==='Enter')sendChat()">
|
||||||
|
<button onclick="sendChat()" id="chatSendBtn">➤</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 챗봇 토글 버튼 -->
|
||||||
|
<button class="chatbot-toggle" id="chatbotToggle" onclick="toggleChatbot()">🐾</button>
|
||||||
|
|
||||||
<!-- QR 인쇄 모달 -->
|
<!-- QR 인쇄 모달 -->
|
||||||
<div class="modal-overlay" id="qrModal" onclick="if(event.target===this)closeQRModal()">
|
<div class="modal-overlay" id="qrModal" onclick="if(event.target===this)closeQRModal()">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
@ -614,6 +855,171 @@
|
|||||||
|
|
||||||
// 페이지 로드 시 검색창 포커스
|
// 페이지 로드 시 검색창 포커스
|
||||||
document.getElementById('searchInput').focus();
|
document.getElementById('searchInput').focus();
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// 동물약 챗봇
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
let chatHistory = [];
|
||||||
|
let isChatLoading = false;
|
||||||
|
|
||||||
|
function toggleChatbot() {
|
||||||
|
const panel = document.getElementById('chatbotPanel');
|
||||||
|
const btn = document.getElementById('chatbotToggle');
|
||||||
|
const isOpen = panel.classList.toggle('open');
|
||||||
|
btn.classList.toggle('active', isOpen);
|
||||||
|
btn.innerHTML = isOpen ? '✕' : '🐾';
|
||||||
|
if (isOpen) {
|
||||||
|
document.getElementById('chatInput').focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendSuggestion(text) {
|
||||||
|
document.getElementById('chatInput').value = text;
|
||||||
|
sendChat();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendChat() {
|
||||||
|
const input = document.getElementById('chatInput');
|
||||||
|
const message = input.value.trim();
|
||||||
|
|
||||||
|
if (!message || isChatLoading) return;
|
||||||
|
|
||||||
|
// 사용자 메시지 표시
|
||||||
|
addChatMessage('user', message);
|
||||||
|
input.value = '';
|
||||||
|
|
||||||
|
// 히스토리에 추가
|
||||||
|
chatHistory.push({ role: 'user', content: message });
|
||||||
|
|
||||||
|
// 로딩 표시
|
||||||
|
isChatLoading = true;
|
||||||
|
document.getElementById('chatSendBtn').disabled = true;
|
||||||
|
showTypingIndicator();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/animal-chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ messages: chatHistory })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
hideTypingIndicator();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// AI 응답 표시
|
||||||
|
addChatMessage('assistant', data.message, data.products);
|
||||||
|
|
||||||
|
// 히스토리에 추가
|
||||||
|
chatHistory.push({ role: 'assistant', content: data.message });
|
||||||
|
|
||||||
|
// 히스토리 길이 제한 (최근 20개)
|
||||||
|
if (chatHistory.length > 20) {
|
||||||
|
chatHistory = chatHistory.slice(-20);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addChatMessage('system', '⚠️ ' + (data.message || '오류가 발생했습니다'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
hideTypingIndicator();
|
||||||
|
addChatMessage('system', '⚠️ 네트워크 오류가 발생했습니다');
|
||||||
|
}
|
||||||
|
|
||||||
|
isChatLoading = false;
|
||||||
|
document.getElementById('chatSendBtn').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addChatMessage(role, content, products) {
|
||||||
|
const container = document.getElementById('chatMessages');
|
||||||
|
const msgDiv = document.createElement('div');
|
||||||
|
msgDiv.className = `chat-message ${role}`;
|
||||||
|
|
||||||
|
// 줄바꿈 처리
|
||||||
|
let htmlContent = escapeHtml(content).replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
// 마크다운 굵게 처리
|
||||||
|
htmlContent = htmlContent.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
|
||||||
|
msgDiv.innerHTML = htmlContent;
|
||||||
|
|
||||||
|
// 언급된 제품 표시 (이미지 포함)
|
||||||
|
if (products && products.length > 0) {
|
||||||
|
const productsDiv = document.createElement('div');
|
||||||
|
productsDiv.className = 'products-mentioned';
|
||||||
|
productsDiv.innerHTML = '<small style="color:#64748b;">📦 관련 제품:</small>';
|
||||||
|
|
||||||
|
const productsGrid = document.createElement('div');
|
||||||
|
productsGrid.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;';
|
||||||
|
|
||||||
|
products.forEach(p => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'product-card-mini';
|
||||||
|
card.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px;background:#f8fafc;border-radius:8px;cursor:pointer;border:1px solid #e2e8f0;';
|
||||||
|
card.onclick = () => searchProductFromChat(p.name);
|
||||||
|
|
||||||
|
// 이미지 컨테이너
|
||||||
|
const imgContainer = document.createElement('div');
|
||||||
|
imgContainer.style.cssText = 'width:40px;height:40px;flex-shrink:0;';
|
||||||
|
|
||||||
|
if (p.image_url) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.style.cssText = 'width:40px;height:40px;object-fit:cover;border-radius:4px;background:#e2e8f0;';
|
||||||
|
img.src = p.image_url;
|
||||||
|
img.alt = p.name;
|
||||||
|
img.onerror = function() {
|
||||||
|
// 이미지 로드 실패 시 아이콘으로 대체
|
||||||
|
imgContainer.innerHTML = '<div style="width:40px;height:40px;background:#f1f5f9;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:20px;">💊</div>';
|
||||||
|
};
|
||||||
|
imgContainer.appendChild(img);
|
||||||
|
} else {
|
||||||
|
// 이미지 없으면 아이콘
|
||||||
|
imgContainer.innerHTML = '<div style="width:40px;height:40px;background:#f1f5f9;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:20px;">💊</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 텍스트
|
||||||
|
const textDiv = document.createElement('div');
|
||||||
|
textDiv.innerHTML = `<div style="font-size:13px;font-weight:500;color:#334155;">${p.name}</div><div style="font-size:12px;color:#10b981;">${formatPrice(p.price)}</div>`;
|
||||||
|
|
||||||
|
card.appendChild(imgContainer);
|
||||||
|
card.appendChild(textDiv);
|
||||||
|
productsGrid.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
productsDiv.appendChild(productsGrid);
|
||||||
|
msgDiv.appendChild(productsDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(msgDiv);
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTypingIndicator() {
|
||||||
|
const container = document.getElementById('chatMessages');
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.className = 'typing-indicator';
|
||||||
|
indicator.id = 'typingIndicator';
|
||||||
|
indicator.innerHTML = '<span></span><span></span><span></span>';
|
||||||
|
container.appendChild(indicator);
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideTypingIndicator() {
|
||||||
|
const indicator = document.getElementById('typingIndicator');
|
||||||
|
if (indicator) indicator.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchProductFromChat(productName) {
|
||||||
|
// 챗봇에서 제품 클릭 시 검색창에 입력하고 검색
|
||||||
|
document.getElementById('searchInput').value = productName;
|
||||||
|
document.getElementById('animalOnly').checked = true;
|
||||||
|
searchProducts();
|
||||||
|
|
||||||
|
// 모바일에서 챗봇 닫기
|
||||||
|
if (window.innerWidth <= 1100) {
|
||||||
|
document.getElementById('chatbotPanel').classList.remove('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user