pharmacy-pos-qr-system/backend/templates/admin_ai_crm.html
thug0bin 9bd2174501 feat: 제품 검색 페이지 및 QR 라벨 인쇄 기능
- /admin/products: 전체 제품 검색 페이지 (OTC)
- /api/products: 제품 검색 API (세트상품 바코드 포함)
- qr_printer.py: Brother QL-710W 프린터 연동
- /api/qr-print, /api/qr-preview: QR 라벨 인쇄/미리보기 API
- 판매상세 페이지에 QR 인쇄 버튼 추가
- 수량 선택 UI (+/- 버튼, 최대 10장)
- 세트상품 제조사 표시 개선
- 대시보드 헤더에 제품검색/판매조회 탭 추가
2026-02-27 13:56:26 +09:00

419 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 업셀링 CRM - 청춘약국</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
background: #f8fafc;
-webkit-font-smoothing: antialiased;
color: #1e293b;
}
/* ── 헤더 ── */
.header {
background: linear-gradient(135deg, #7c3aed 0%, #6366f1 50%, #8b5cf6 100%);
padding: 28px 32px 24px;
color: #fff;
}
.header-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.header-nav a {
color: rgba(255,255,255,0.8);
text-decoration: none;
font-size: 14px;
font-weight: 500;
}
.header-nav a:hover { color: #fff; }
.header h1 {
font-size: 24px;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 6px;
}
.header p {
font-size: 14px;
opacity: 0.85;
font-weight: 400;
}
/* ── 컨텐츠 ── */
.content {
max-width: 1100px;
margin: 0 auto;
padding: 24px 20px 60px;
}
/* ── 통계 카드 ── */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 28px;
}
.stat-card {
background: #fff;
border-radius: 14px;
padding: 20px;
border: 1px solid #e2e8f0;
}
.stat-label {
font-size: 12px;
font-weight: 600;
color: #94a3b8;
letter-spacing: -0.2px;
margin-bottom: 8px;
text-transform: uppercase;
}
.stat-value {
font-size: 32px;
font-weight: 700;
letter-spacing: -1px;
}
.stat-value.default { color: #1e293b; }
.stat-value.green { color: #16a34a; }
.stat-value.orange { color: #d97706; }
.stat-value.indigo { color: #6366f1; }
/* ── 테이블 섹션 ── */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.section-title {
font-size: 17px;
font-weight: 700;
color: #1e293b;
letter-spacing: -0.3px;
}
.section-sub {
font-size: 13px;
color: #94a3b8;
font-weight: 500;
}
.table-wrap {
background: #fff;
border-radius: 14px;
border: 1px solid #e2e8f0;
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
background: #f8fafc;
padding: 12px 14px;
font-size: 12px;
font-weight: 600;
color: #64748b;
text-align: left;
border-bottom: 1px solid #e2e8f0;
letter-spacing: -0.2px;
white-space: nowrap;
}
tbody td {
padding: 14px;
font-size: 13px;
font-weight: 500;
color: #334155;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
tbody tr { cursor: pointer; transition: background .15s; }
tbody tr:hover { background: #f8fafc; }
tbody tr:last-child td { border-bottom: none; }
/* ── 배지 ── */
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 100px;
font-size: 11px;
font-weight: 600;
letter-spacing: -0.2px;
}
.badge-active { background: #dcfce7; color: #16a34a; }
.badge-interested { background: #fef3c7; color: #d97706; }
.badge-dismissed { background: #f1f5f9; color: #64748b; }
.badge-expired { background: #fee2e2; color: #dc2626; }
.badge-trigger {
background: #dbeafe;
color: #2563eb;
margin: 1px 2px;
font-size: 11px;
padding: 2px 8px;
}
.badge-product {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: #fff;
padding: 4px 12px;
font-size: 12px;
}
/* ── 메시지 말줄임 ── */
.msg-ellipsis {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #64748b;
font-size: 12px;
}
/* ── 노출 횟수 ── */
.display-count {
text-align: center;
font-weight: 700;
color: #6366f1;
font-size: 14px;
}
.display-count.zero { color: #cbd5e1; }
/* ── 아코디언 상세 ── */
.detail-row { display: none; }
.detail-row.open { display: table-row; }
.detail-row td {
padding: 0;
border-bottom: 1px solid #e2e8f0;
}
.detail-content {
padding: 20px 24px;
background: #fafbfd;
}
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.detail-field {
margin-bottom: 2px;
}
.detail-label {
font-size: 11px;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
margin-bottom: 4px;
letter-spacing: 0.3px;
}
.detail-value {
font-size: 13px;
font-weight: 500;
color: #334155;
line-height: 1.5;
}
.detail-raw {
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid #e2e8f0;
}
.detail-raw pre {
background: #1e293b;
color: #e2e8f0;
padding: 14px 16px;
border-radius: 10px;
font-size: 12px;
line-height: 1.6;
overflow-x: auto;
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
white-space: pre-wrap;
word-break: break-all;
}
/* ── 빈 상태 ── */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #94a3b8;
}
.empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.5; }
.empty-text { font-size: 14px; font-weight: 500; }
/* ── 반응형 ── */
@media (max-width: 768px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.detail-grid { grid-template-columns: 1fr; }
.header { padding: 20px 16px 18px; }
.content { padding: 16px 12px 40px; }
.table-wrap { overflow-x: auto; }
table { min-width: 700px; }
}
</style>
</head>
<body>
<div class="header">
<div class="header-nav">
<a href="/admin">← 관리자 홈</a>
<div>
<a href="/admin/ai-gw" style="margin-right: 16px;">Gateway 모니터</a>
<a href="/admin/alimtalk">알림톡 로그 →</a>
</div>
</div>
<h1>AI 업셀링 CRM</h1>
<p>구매 기반 맞춤 추천 생성 현황 · Clawdbot Gateway</p>
</div>
<div class="content">
<!-- 통계 카드 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">전체 생성</div>
<div class="stat-value default">{{ stats.total or 0 }}</div>
</div>
<div class="stat-card">
<div class="stat-label">Active</div>
<div class="stat-value green">{{ stats.active_count or 0 }}</div>
</div>
<div class="stat-card">
<div class="stat-label">관심있어요</div>
<div class="stat-value orange">{{ stats.interested_count or 0 }}</div>
</div>
<div class="stat-card">
<div class="stat-label">오늘 생성</div>
<div class="stat-value indigo">{{ stats.today_count or 0 }}</div>
</div>
</div>
<!-- 추천 목록 -->
<div class="section-header">
<div class="section-title">추천 생성 로그</div>
<div class="section-sub">최근 50건 · 클릭하여 상세 보기</div>
</div>
{% if recs %}
<div class="table-wrap">
<table>
<thead>
<tr>
<th>생성일시</th>
<th>고객</th>
<th>트리거 품목</th>
<th>추천 제품</th>
<th>AI 메시지</th>
<th>상태</th>
<th style="text-align:center">노출</th>
</tr>
</thead>
<tbody>
{% for rec in recs %}
<tr onclick="toggleDetail({{ rec.id }})">
<td style="white-space:nowrap;font-size:12px;color:#64748b;">
{{ rec.created_at[5:16] if rec.created_at else '-' }}
</td>
<td>
<div style="font-weight:600;font-size:13px;">{{ rec.nickname or '알 수 없음' }}</div>
{% if rec.user_phone %}
<div style="font-size:11px;color:#94a3b8;">{{ rec.user_phone[:3] }}-****-{{ rec.user_phone[-4:] }}</div>
{% endif %}
</td>
<td>
{% if rec.trigger_list %}
{% for item in rec.trigger_list %}
<span class="badge badge-trigger">{{ item }}</span>
{% endfor %}
{% else %}
<span style="color:#cbd5e1;">-</span>
{% endif %}
</td>
<td>
<span class="badge badge-product">{{ rec.recommended_product }}</span>
</td>
<td>
<div class="msg-ellipsis" title="{{ rec.recommendation_message }}">{{ rec.recommendation_message }}</div>
</td>
<td>
{% if rec.status == 'interested' %}
<span class="badge badge-interested">관심있어요</span>
{% elif rec.status == 'active' and (not rec.expires_at or rec.expires_at > now) %}
<span class="badge badge-active">Active</span>
{% elif rec.status == 'dismissed' %}
<span class="badge badge-dismissed">Dismissed</span>
{% else %}
<span class="badge badge-expired">Expired</span>
{% endif %}
</td>
<td class="display-count {{ 'zero' if not rec.displayed_count else '' }}">
{{ rec.displayed_count or 0 }}
</td>
</tr>
<!-- 상세 아코디언 -->
<tr class="detail-row" id="detail-{{ rec.id }}">
<td colspan="7">
<div class="detail-content">
<div class="detail-grid">
<div class="detail-field">
<div class="detail-label">추천 이유</div>
<div class="detail-value">{{ rec.recommendation_reason or '-' }}</div>
</div>
<div class="detail-field">
<div class="detail-label">거래 ID</div>
<div class="detail-value">{{ rec.transaction_id or '-' }}</div>
</div>
<div class="detail-field">
<div class="detail-label">노출 일시</div>
<div class="detail-value">{{ rec.displayed_at or '미노출' }}</div>
</div>
<div class="detail-field">
<div class="detail-label">닫기 일시</div>
<div class="detail-value">{{ rec.dismissed_at or '-' }}</div>
</div>
<div class="detail-field">
<div class="detail-label">만료 일시</div>
<div class="detail-value">{{ rec.expires_at or '없음' }}</div>
</div>
<div class="detail-field">
<div class="detail-label">노출 횟수</div>
<div class="detail-value">{{ rec.displayed_count or 0 }}회</div>
</div>
</div>
{% if rec.ai_raw_response %}
<div class="detail-raw">
<div class="detail-label">AI 원본 응답</div>
<pre>{{ rec.ai_raw_response }}</pre>
</div>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="table-wrap">
<div class="empty-state">
<div class="empty-icon">🤖</div>
<div class="empty-text">아직 생성된 AI 추천이 없습니다</div>
</div>
</div>
{% endif %}
</div>
<script>
function toggleDetail(id) {
const row = document.getElementById('detail-' + id);
if (!row) return;
// 다른 열린 것 닫기
document.querySelectorAll('.detail-row.open').forEach(function(el) {
if (el.id !== 'detail-' + id) el.classList.remove('open');
});
row.classList.toggle('open');
}
</script>
</body>
</html>