2145 lines
106 KiB
HTML
2145 lines
106 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>관리자 페이지 - 청춘약국</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;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
background: #f5f7fa;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
/* 토스트 알림 */
|
||
.toast-container {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 10000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
.toast {
|
||
padding: 14px 20px;
|
||
border-radius: 10px;
|
||
color: white;
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.toast.success { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
|
||
.toast.error { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); }
|
||
.toast.info { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); }
|
||
.toast.printing { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); }
|
||
@keyframes toastIn {
|
||
from { opacity: 0; transform: translateX(100px); }
|
||
to { opacity: 1; transform: translateX(0); }
|
||
}
|
||
@keyframes toastOut {
|
||
from { opacity: 1; transform: translateX(0); }
|
||
to { opacity: 0; transform: translateX(100px); }
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||
padding: 32px 24px;
|
||
color: #ffffff;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.header-content {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
margin-bottom: 8px;
|
||
letter-spacing: -0.5px;
|
||
}
|
||
|
||
.header-subtitle {
|
||
font-size: 15px;
|
||
opacity: 0.9;
|
||
font-weight: 500;
|
||
letter-spacing: -0.2px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 24px;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: #ffffff;
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||
}
|
||
|
||
.stat-label {
|
||
color: #868e96;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
margin-bottom: 12px;
|
||
letter-spacing: -0.2px;
|
||
}
|
||
|
||
.stat-value {
|
||
color: #212529;
|
||
font-size: 36px;
|
||
font-weight: 700;
|
||
letter-spacing: -1px;
|
||
}
|
||
|
||
.stat-value.primary {
|
||
color: #6366f1;
|
||
}
|
||
|
||
.section {
|
||
background: #ffffff;
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
margin-bottom: 24px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #212529;
|
||
margin-bottom: 20px;
|
||
letter-spacing: -0.3px;
|
||
}
|
||
|
||
.table-responsive {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
th {
|
||
text-align: left;
|
||
padding: 12px 16px;
|
||
background: #f8f9fa;
|
||
color: #495057;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
border-bottom: 2px solid #e9ecef;
|
||
letter-spacing: -0.2px;
|
||
}
|
||
|
||
td {
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid #f1f3f5;
|
||
color: #495057;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
letter-spacing: -0.2px;
|
||
}
|
||
|
||
tr:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
border-radius: 100px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.2px;
|
||
}
|
||
|
||
.badge-success {
|
||
background: #d3f9d8;
|
||
color: #2b8a3e;
|
||
}
|
||
|
||
.badge-warning {
|
||
background: #fff3bf;
|
||
color: #e67700;
|
||
}
|
||
|
||
.points-positive {
|
||
color: #6366f1;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.phone-masked {
|
||
font-family: 'Courier New', monospace;
|
||
color: #495057;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.stats-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.table-responsive {
|
||
font-size: 12px;
|
||
}
|
||
|
||
th, td {
|
||
padding: 10px 12px;
|
||
}
|
||
}
|
||
|
||
/* 아코디언 스타일 */
|
||
.accordion-content {
|
||
transition: max-height 0.3s ease-out;
|
||
}
|
||
|
||
/* 탭 스타일 */
|
||
.tab-btn {
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.tab-btn:hover {
|
||
color: #6366f1 !important;
|
||
}
|
||
|
||
/* 사용자 테이블 행 호버 */
|
||
.section table tbody tr {
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.section table tbody tr:hover {
|
||
background: #f8f9fa;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.section table tbody tr[onclick]:hover {
|
||
background: #eef2ff;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* 사이드바 레이아웃 */
|
||
.layout-wrapper {
|
||
display: flex;
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.sidebar {
|
||
width: 280px;
|
||
background: #ffffff;
|
||
min-height: calc(100vh - 112px);
|
||
padding: 24px 16px;
|
||
box-shadow: 2px 0 8px rgba(0,0,0,0.04);
|
||
}
|
||
|
||
.sidebar-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #212529;
|
||
margin-bottom: 20px;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.search-container {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.search-type-toggle {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.search-type-btn {
|
||
flex: 1;
|
||
padding: 10px;
|
||
border: 2px solid #e9ecef;
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #868e96;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.search-type-btn.active {
|
||
border-color: #6366f1;
|
||
background: #f8f9ff;
|
||
color: #6366f1;
|
||
}
|
||
|
||
.sort-btn {
|
||
padding: 8px 16px;
|
||
border: 2px solid #e9ecef;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #868e96;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.sort-btn:hover {
|
||
border-color: #ced4da;
|
||
}
|
||
|
||
.sort-btn.active {
|
||
border-color: #6366f1;
|
||
background: #f8f9ff;
|
||
color: #6366f1;
|
||
}
|
||
|
||
.search-input-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.search-input {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
border: 2px solid #e9ecef;
|
||
border-radius: 12px;
|
||
font-size: 14px;
|
||
font-family: 'Noto Sans KR', sans-serif;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.search-input:focus {
|
||
outline: none;
|
||
border-color: #6366f1;
|
||
box-shadow: 0 0 0 4px rgba(99,102,241,0.08);
|
||
}
|
||
|
||
.search-btn {
|
||
width: 100%;
|
||
padding: 12px;
|
||
margin-top: 8px;
|
||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 12px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.search-btn:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(99,102,241,0.3);
|
||
}
|
||
|
||
.search-btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
/* 검색 결과 모달 */
|
||
.search-results-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0,0,0,0.5);
|
||
z-index: 10000;
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.search-results-content {
|
||
max-width: 900px;
|
||
margin: 40px auto;
|
||
background: #fff;
|
||
border-radius: 20px;
|
||
padding: 32px;
|
||
position: relative;
|
||
}
|
||
|
||
.container {
|
||
flex: 1;
|
||
padding: 24px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.sidebar {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
/* 카테고리 뱃지 스타일 */
|
||
.category-badge {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
margin: 2px 4px 2px 0;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: #ffffff;
|
||
box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.category-badge:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
/* 제품 카테고리별 색상 */
|
||
.category-badge.cat-진통제 { background: linear-gradient(135deg, #f06292 0%, #e91e63 100%); }
|
||
.category-badge.cat-소화제 { background: linear-gradient(135deg, #64b5f6 0%, #1976d2 100%); }
|
||
.category-badge.cat-감기약 { background: linear-gradient(135deg, #4db6ac 0%, #00796b 100%); }
|
||
.category-badge.cat-복합비타민 { background: linear-gradient(135deg, #ffb74d 0%, #f57c00 100%); }
|
||
.category-badge.cat-피로회복제 { background: linear-gradient(135deg, #a1887f 0%, #6d4c41 100%); }
|
||
.category-badge.cat-소염제 { background: linear-gradient(135deg, #ff8a65 0%, #d84315 100%); }
|
||
.category-badge.cat-연고 { background: linear-gradient(135deg, #90a4ae 0%, #546e7a 100%); }
|
||
.category-badge.cat-파스 { background: linear-gradient(135deg, #81c784 0%, #388e3c 100%); }
|
||
.category-badge.cat-간영양제 { background: linear-gradient(135deg, #ba68c8 0%, #8e24aa 100%); }
|
||
.category-badge.cat-위장약 { background: linear-gradient(135deg, #4fc3f7 0%, #0288d1 100%); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<div class="header-content" style="display:flex;justify-content:space-between;align-items:center;">
|
||
<div>
|
||
<div class="header-title">📊 관리자 대시보드</div>
|
||
<div class="header-subtitle">청춘약국 마일리지 관리</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px;">
|
||
<a href="/admin/products" style="color:rgba(255,255,255,0.9);text-decoration:none;font-size:14px;padding:8px 16px;border-radius:8px;background:rgba(255,255,255,0.15);transition:all 0.2s;">🔍 제품검색</a>
|
||
<a href="/admin/members" style="color:rgba(255,255,255,0.9);text-decoration:none;font-size:14px;padding:8px 16px;border-radius:8px;background:rgba(255,255,255,0.15);transition:all 0.2s;">👥 회원검색</a>
|
||
<a href="/admin/sales-detail" style="color:rgba(255,255,255,0.9);text-decoration:none;font-size:14px;padding:8px 16px;border-radius:8px;background:rgba(255,255,255,0.15);transition:all 0.2s;">📋 판매조회</a>
|
||
<a href="/admin/sales" style="color:rgba(255,255,255,0.9);text-decoration:none;font-size:14px;padding:8px 16px;border-radius:8px;background:rgba(255,255,255,0.15);transition:all 0.2s;">🧾 판매내역</a>
|
||
<a href="/admin/ai-crm" style="color:rgba(255,255,255,0.9);text-decoration:none;font-size:14px;padding:8px 16px;border-radius:8px;background:rgba(255,255,255,0.15);transition:all 0.2s;">🤖 AI CRM</a>
|
||
<a href="/admin/alimtalk" style="color:rgba(255,255,255,0.9);text-decoration:none;font-size:14px;padding:8px 16px;border-radius:8px;background:rgba(255,255,255,0.15);transition:all 0.2s;">📨 알림톡</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="layout-wrapper">
|
||
<!-- 사이드바 -->
|
||
<aside class="sidebar">
|
||
<div class="sidebar-title">🔍 검색</div>
|
||
|
||
<div class="search-container">
|
||
<div class="search-type-toggle">
|
||
<button class="search-type-btn active" onclick="switchSearchType('user')" id="btn-search-user">
|
||
사용자
|
||
</button>
|
||
<button class="search-type-btn" onclick="switchSearchType('product')" id="btn-search-product">
|
||
제품
|
||
</button>
|
||
</div>
|
||
|
||
<div class="search-input-wrapper">
|
||
<input type="text" class="search-input" id="searchInput" placeholder="이름 또는 전화번호 입력" onkeypress="if(event.key==='Enter') performSearch()">
|
||
</div>
|
||
|
||
<button class="search-btn" onclick="performSearch()">검색</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<div class="container">
|
||
<!-- 전체 통계 -->
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-label">총 가입자 수</div>
|
||
<div class="stat-value primary">{{ stats.total_users or 0 }}명</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">누적 포인트 잔액</div>
|
||
<div class="stat-value primary">{{ "{:,}".format(stats.total_balance or 0) }}P</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">QR 발행 건수</div>
|
||
<div class="stat-value">{{ token_stats.total_tokens or 0 }}건</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">적립 완료율</div>
|
||
<div class="stat-value">
|
||
{% if token_stats.total_tokens and token_stats.total_tokens > 0 %}
|
||
{{ "%.1f"|format((token_stats.claimed_count or 0) * 100.0 / token_stats.total_tokens) }}%
|
||
{% else %}
|
||
0%
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="stat-card" style="background: linear-gradient(135deg, #fef3c7, #fde68a);">
|
||
<div class="stat-label" style="color: #92400e;">🐾 등록 반려동물</div>
|
||
<div class="stat-value" style="color: #92400e;">
|
||
{{ pet_stats.total_pets or 0 }}마리
|
||
<span style="font-size: 14px; font-weight: 500; margin-left: 8px;">
|
||
(🐕 {{ pet_stats.dog_count or 0 }} / 🐈 {{ pet_stats.cat_count or 0 }})
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 최근 등록 반려동물 -->
|
||
{% if recent_pets %}
|
||
<div class="section">
|
||
<div class="section-title">🐾 최근 등록 반려동물 (10마리)</div>
|
||
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
|
||
{% for pet in recent_pets %}
|
||
<div style="display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: linear-gradient(135deg, #fef3c7, #fde68a); border-radius: 14px; min-width: 220px;">
|
||
{% if pet.photo_url %}
|
||
<img src="{{ pet.photo_url }}" style="width: 48px; height: 48px; border-radius: 50%; object-fit: cover; border: 2px solid #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.15);">
|
||
{% else %}
|
||
<div style="width: 48px; height: 48px; border-radius: 50%; background: #fff; display: flex; align-items: center; justify-content: center; font-size: 24px; box-shadow: 0 2px 6px rgba(0,0,0,0.1);">
|
||
{% if pet.species == 'dog' %}🐕{% elif pet.species == 'cat' %}🐈{% else %}🐾{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
<div>
|
||
<div style="font-weight: 700; font-size: 15px; color: #92400e;">
|
||
{% if pet.species == 'dog' %}🐕{% elif pet.species == 'cat' %}🐈{% else %}🐾{% endif %} {{ pet.name }}
|
||
</div>
|
||
<div style="font-size: 12px; color: #a16207;">{{ pet.breed or '품종 미등록' }}</div>
|
||
<div style="font-size: 11px; color: #b45309; margin-top: 2px;">{{ pet.owner_name }} ({{ pet.owner_phone[:3] }}-****-{{ pet.owner_phone[-4:] }})</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- 최근 가입 사용자 -->
|
||
<div class="section">
|
||
<div class="section-title">최근 가입 사용자 (20명)</div>
|
||
<div class="table-responsive">
|
||
{% if recent_users %}
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>이름</th>
|
||
<th>전화번호</th>
|
||
<th>포인트</th>
|
||
<th>가입일</th>
|
||
<th>카카오</th>
|
||
<th>조제</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for user in recent_users %}
|
||
<tr style="cursor: pointer;" onclick="showUserDetail({{ user.id }})">
|
||
<td>{{ user.id }}</td>
|
||
<td style="color: #6366f1; font-weight: 600;">{{ user.nickname }}</td>
|
||
<td class="phone-masked">{{ user.phone[:3] }}-{{ user.phone[3:7] }}-{{ user.phone[7:] if user.phone|length > 7 else '' }}</td>
|
||
<td class="points-positive">{{ "{:,}".format(user.mileage_balance) }}P</td>
|
||
<td>{{ user.created_at[:16].replace('T', ' ') }}</td>
|
||
<td>
|
||
{% if user.kakao_verified_at %}
|
||
<span style="display: inline-flex; align-items: center; gap: 3px; background: #FEE500; color: #3C1E1E; font-size: 10px; font-weight: 700; padding: 3px 8px; border-radius: 10px;">💬 {{ user.kakao_verified_at[:10] }}</span>
|
||
{% else %}
|
||
<span style="display: inline-flex; align-items: center; background: #f1f3f5; color: #868e96; font-size: 10px; font-weight: 600; padding: 3px 8px; border-radius: 10px;">미인증</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if user.has_prescription %}
|
||
<span style="display: inline-flex; align-items: center; gap: 3px; background: #d3f9d8; color: #2b8a3e; font-size: 10px; font-weight: 700; padding: 3px 8px; border-radius: 10px;">💊 환자</span>
|
||
{% else %}
|
||
<span style="display: inline-flex; align-items: center; background: #f1f3f5; color: #adb5bd; font-size: 10px; font-weight: 600; padding: 3px 8px; border-radius: 10px;">일반</span>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<p style="text-align: center; padding: 40px; color: #868e96;">가입한 사용자가 없습니다.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 최근 적립 내역 -->
|
||
<div class="section">
|
||
<div class="section-title">최근 적립 내역 (50건)</div>
|
||
<div class="table-responsive">
|
||
{% if recent_transactions %}
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>이름</th>
|
||
<th>전화번호</th>
|
||
<th>포인트</th>
|
||
<th>잔액</th>
|
||
<th>내역</th>
|
||
<th>일시</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for tx in recent_transactions %}
|
||
<tr{% if tx.transaction_id %} onclick="showTransactionDetail('{{ tx.transaction_id }}')" style="cursor: pointer;" title="클릭하여 품목 상세 보기"{% endif %}>
|
||
<td>{{ tx.nickname }}</td>
|
||
<td class="phone-masked">{{ tx.phone[:3] }}-{{ tx.phone[3:7] }}-{{ tx.phone[7:] if tx.phone|length > 7 else '' }}</td>
|
||
<td class="points-positive">{{ "{:,}".format(tx.points) }}P</td>
|
||
<td>{{ "{:,}".format(tx.balance_after) }}P</td>
|
||
<td>{{ tx.description or tx.reason }}{% if tx.transaction_id %} <span style="color: #6366f1; font-size: 12px;">🔍</span>{% endif %}</td>
|
||
<td>{{ tx.created_at[:16].replace('T', ' ') }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<p style="text-align: center; padding: 40px; color: #868e96;">적립 내역이 없습니다.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 최근 QR 발행 내역 -->
|
||
<div class="section">
|
||
<div class="section-title">최근 QR 발행 내역 (20건)</div>
|
||
<div class="table-responsive">
|
||
{% if recent_tokens %}
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>거래번호</th>
|
||
<th>판매금액</th>
|
||
<th>적립포인트</th>
|
||
<th>상태</th>
|
||
<th>발행일</th>
|
||
<th>적립일</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for token in recent_tokens %}
|
||
<tr>
|
||
<td>
|
||
<a href="javascript:void(0)" onclick="showTransactionDetail('{{ token.transaction_id }}')" style="color: #6366f1; text-decoration: none; font-weight: 600; cursor: pointer;">
|
||
{{ token.transaction_id }}
|
||
</a>
|
||
</td>
|
||
<td>{{ "{:,}".format(token.total_amount) }}원</td>
|
||
<td class="points-positive">{{ "{:,}".format(token.claimable_points) }}P</td>
|
||
<td>
|
||
{% if token.claimed_at %}
|
||
<span class="badge badge-success">적립완료</span>
|
||
{% else %}
|
||
<span class="badge badge-warning">대기중</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ token.created_at[:16].replace('T', ' ') }}</td>
|
||
<td>{{ token.claimed_at[:16].replace('T', ' ') if token.claimed_at else '-' }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<p style="text-align: center; padding: 40px; color: #868e96;">발행된 QR이 없습니다.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div> <!-- .container -->
|
||
</div> <!-- .layout-wrapper -->
|
||
|
||
<!-- 검색 결과 모달 -->
|
||
<div id="searchResultsModal" class="search-results-modal">
|
||
<div class="search-results-content">
|
||
<button onclick="closeSearchResults()" style="position: absolute; top: 20px; right: 20px; background: #f1f3f5; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px; color: #495057;">×</button>
|
||
|
||
<h2 style="font-size: 24px; font-weight: 700; color: #212529; margin-bottom: 24px; letter-spacing: -0.5px;" id="searchResultTitle">검색 결과</h2>
|
||
|
||
<div id="searchResultContent" style="min-height: 200px;">
|
||
<div style="text-align: center; padding: 60px; color: #868e96;">
|
||
<div style="font-size: 14px;">검색 결과가 여기에 표시됩니다.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 거래 세부 내역 모달 -->
|
||
<div id="transactionModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999; padding: 20px; overflow-y: auto;">
|
||
<div style="max-width: 800px; margin: 40px auto; background: #fff; border-radius: 20px; padding: 32px; position: relative;">
|
||
<button onclick="closeModal()" style="position: absolute; top: 20px; right: 20px; background: #f1f3f5; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px; color: #495057;">×</button>
|
||
|
||
<h2 style="font-size: 24px; font-weight: 700; color: #212529; margin-bottom: 24px; letter-spacing: -0.5px;">판매 내역 상세</h2>
|
||
|
||
<div id="transactionContent" style="min-height: 200px;">
|
||
<div style="text-align: center; padding: 60px; color: #868e96;">
|
||
<div style="font-size: 14px;">불러오는 중...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 사용자 상세 모달 -->
|
||
<div id="userDetailModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999; padding: 20px; overflow-y: auto;">
|
||
<div style="max-width: 900px; margin: 40px auto; background: #fff; border-radius: 20px; padding: 32px; position: relative;">
|
||
<button onclick="closeUserModal()" style="position: absolute; top: 20px; right: 20px; background: #f1f3f5; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px; color: #495057;">×</button>
|
||
|
||
<h2 style="font-size: 24px; font-weight: 700; color: #212529; margin-bottom: 24px; letter-spacing: -0.5px;">사용자 상세 정보</h2>
|
||
|
||
<div id="userDetailContent" style="min-height: 200px;">
|
||
<div style="text-align: center; padding: 60px; color: #868e96;">
|
||
<div style="font-size: 14px;">불러오는 중...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 포인트 사용 모달 -->
|
||
<div id="usePointsModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 10000; padding: 20px; overflow-y: auto;">
|
||
<div style="max-width: 500px; margin: 100px auto; background: #fff; border-radius: 20px; padding: 32px; position: relative;">
|
||
<button onclick="closeUsePointsModal()" style="position: absolute; top: 20px; right: 20px; background: #f1f3f5; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px; color: #495057;">×</button>
|
||
|
||
<h2 style="font-size: 24px; font-weight: 700; color: #212529; margin-bottom: 24px; letter-spacing: -0.5px;">💳 포인트 사용</h2>
|
||
|
||
<div style="background: #f8f9fa; border-radius: 12px; padding: 16px; margin-bottom: 24px;">
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">현재 포인트 잔액</div>
|
||
<div id="current-balance-display" style="color: #6366f1; font-size: 24px; font-weight: 700;">0P</div>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 24px;">
|
||
<label style="display: block; color: #495057; font-size: 14px; font-weight: 600; margin-bottom: 8px;">사용할 포인트</label>
|
||
<input type="number" id="points-to-use" placeholder="포인트 입력" min="1" style="width: 100%; padding: 14px; border: 2px solid #e9ecef; border-radius: 10px; font-size: 16px; font-family: 'Noto Sans KR', sans-serif; box-sizing: border-box;" onkeypress="if(event.key==='Enter') confirmUsePoints()">
|
||
<div id="use-points-error" style="color: #f03e3e; font-size: 13px; margin-top: 8px; display: none;"></div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 12px;">
|
||
<button onclick="closeUsePointsModal()" style="flex: 1; padding: 14px; background: #f1f3f5; color: #495057; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer;">
|
||
취소
|
||
</button>
|
||
<button onclick="confirmUsePoints()" style="flex: 1; padding: 14px; background: #f03e3e; color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer;">
|
||
사용하기
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AI 분석 모달 -->
|
||
<div id="aiAnalysisModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 10001; padding: 20px; overflow-y: auto;">
|
||
<div style="max-width: 800px; margin: 40px auto; background: #fff; border-radius: 20px; padding: 32px; position: relative;">
|
||
<button onclick="closeAIAnalysisModal()" style="position: absolute; top: 20px; right: 20px; background: #f1f3f5; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px; color: #495057;">×</button>
|
||
|
||
<h2 style="font-size: 24px; font-weight: 700; color: #212529; margin-bottom: 24px; letter-spacing: -0.5px;">
|
||
🤖 AI 구매 패턴 분석
|
||
</h2>
|
||
|
||
<div id="aiAnalysisContent" style="min-height: 200px;">
|
||
<div style="text-align: center; padding: 60px; color: #868e96;">
|
||
<div style="font-size: 14px;">AI 분석을 시작하려면 버튼을 클릭하세요.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/static/js/lottie.min.js"></script>
|
||
<script>
|
||
function showTransactionDetail(transactionId) {
|
||
document.getElementById('transactionModal').style.display = 'block';
|
||
document.getElementById('transactionContent').innerHTML = '<div style="text-align: center; padding: 60px; color: #868e96;"><div style="font-size: 14px;">불러오는 중...</div></div>';
|
||
|
||
fetch(`/admin/transaction/${transactionId}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
renderTransactionDetail(data);
|
||
} else {
|
||
document.getElementById('transactionContent').innerHTML = `
|
||
<div style="text-align: center; padding: 60px; color: #f03e3e;">
|
||
<div style="font-size: 16px; font-weight: 600; margin-bottom: 8px;">오류</div>
|
||
<div style="font-size: 14px;">${data.message}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
document.getElementById('transactionContent').innerHTML = `
|
||
<div style="text-align: center; padding: 60px; color: #f03e3e;">
|
||
<div style="font-size: 16px; font-weight: 600; margin-bottom: 8px;">네트워크 오류</div>
|
||
<div style="font-size: 14px;">데이터를 불러올 수 없습니다.</div>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
function renderTransactionDetail(data) {
|
||
const tx = data.transaction;
|
||
const items = data.items;
|
||
|
||
let html = `
|
||
<div style="background: #f8f9fa; border-radius: 12px; padding: 20px; margin-bottom: 24px;">
|
||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">거래번호</div>
|
||
<div style="color: #212529; font-size: 16px; font-weight: 600;">${tx.id}</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">거래일시</div>
|
||
<div style="color: #212529; font-size: 16px; font-weight: 600;">${tx.date}</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">고객명</div>
|
||
<div style="color: #212529; font-size: 16px; font-weight: 600;">${tx.customer_name}</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">총 금액</div>
|
||
<div style="color: #495057; font-size: 16px; font-weight: 600;">${tx.total_amount.toLocaleString()}원</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">할인</div>
|
||
<div style="color: #f03e3e; font-size: 16px; font-weight: 600;">-${tx.discount.toLocaleString()}원</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">판매 금액</div>
|
||
<div style="color: #6366f1; font-size: 18px; font-weight: 700;">${tx.sale_amount.toLocaleString()}원</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">외상</div>
|
||
<div style="color: #212529; font-size: 16px; font-weight: 600;">${tx.credit.toLocaleString()}원</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">공급가액</div>
|
||
<div style="color: #37b24d; font-size: 16px; font-weight: 600;">${tx.supply_value.toLocaleString()}원</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">부가세</div>
|
||
<div style="color: #495057; font-size: 16px; font-weight: 600;">${tx.vat.toLocaleString()}원</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 16px; font-size: 18px; font-weight: 700; color: #212529;">판매 상품 (${items.length}개)</div>
|
||
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<thead>
|
||
<tr style="background: #f8f9fa; border-bottom: 2px solid #e9ecef;">
|
||
<th style="padding: 12px; text-align: left; font-size: 13px; color: #495057; font-weight: 600;">상품코드</th>
|
||
<th style="padding: 12px; text-align: left; font-size: 13px; color: #495057; font-weight: 600;">상품명</th>
|
||
<th style="padding: 12px; text-align: right; font-size: 13px; color: #495057; font-weight: 600;">수량</th>
|
||
<th style="padding: 12px; text-align: right; font-size: 13px; color: #495057; font-weight: 600;">단가</th>
|
||
<th style="padding: 12px; text-align: right; font-size: 13px; color: #495057; font-weight: 600;">합계</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
items.forEach(item => {
|
||
html += `
|
||
<tr style="border-bottom: 1px solid #f1f3f5;">
|
||
<td style="padding: 14px; font-size: 14px; color: #495057;">${item.code}</td>
|
||
<td style="padding: 14px; font-size: 14px; color: #212529; font-weight: 500;">${item.name}</td>
|
||
<td style="padding: 14px; text-align: right; font-size: 14px; color: #495057;">${item.qty}</td>
|
||
<td style="padding: 14px; text-align: right; font-size: 14px; color: #495057;">${item.price.toLocaleString()}원</td>
|
||
<td style="padding: 14px; text-align: right; font-size: 14px; color: #6366f1; font-weight: 600;">${item.total.toLocaleString()}원</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
document.getElementById('transactionContent').innerHTML = html;
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('transactionModal').style.display = 'none';
|
||
}
|
||
|
||
// ESC 키로 모달 닫기
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
closeModal();
|
||
}
|
||
});
|
||
|
||
// 모달 배경 클릭 시 닫기
|
||
document.getElementById('transactionModal').addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
closeModal();
|
||
}
|
||
});
|
||
|
||
// ===== 사용자 상세 모달 함수 =====
|
||
|
||
// 전역 변수: 현재 사용자 데이터 저장
|
||
let currentUserData = null;
|
||
let currentSortType = 'date'; // 'date' 또는 'amount'
|
||
|
||
function showUserDetail(userId) {
|
||
// 정렬 타입 초기화
|
||
currentSortType = 'date';
|
||
|
||
document.getElementById('userDetailModal').style.display = 'block';
|
||
document.getElementById('userDetailContent').innerHTML = '<div style="text-align: center; padding: 60px; color: #868e96;"><div style="font-size: 14px;">불러오는 중...</div></div>';
|
||
|
||
fetch(`/admin/user/${userId}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
renderUserDetail(data);
|
||
} else {
|
||
document.getElementById('userDetailContent').innerHTML = `
|
||
<div style="text-align: center; padding: 60px; color: #f03e3e;">
|
||
<div style="font-size: 16px; font-weight: 600; margin-bottom: 8px;">오류</div>
|
||
<div style="font-size: 14px;">${data.message}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
document.getElementById('userDetailContent').innerHTML = `
|
||
<div style="text-align: center; padding: 60px; color: #f03e3e;">
|
||
<div style="font-size: 16px; font-weight: 600; margin-bottom: 8px;">네트워크 오류</div>
|
||
<div style="font-size: 14px;">데이터를 불러올 수 없습니다.</div>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
function closeUserModal() {
|
||
document.getElementById('userDetailModal').style.display = 'none';
|
||
}
|
||
|
||
// 특이사항 펼치기/접기 (클릭 시)
|
||
function toggleCusetc(el) {
|
||
if (el.style.maxHeight === 'none' || el.style.maxHeight === '') {
|
||
el.style.maxHeight = '40px';
|
||
el.style.overflow = 'hidden';
|
||
} else {
|
||
el.style.maxHeight = 'none';
|
||
el.style.overflow = 'visible';
|
||
}
|
||
}
|
||
|
||
// 특이사항 수정 모드
|
||
function editCusetc(cuscode, btn) {
|
||
document.getElementById('cusetc-view').style.display = 'none';
|
||
document.getElementById('cusetc-edit').style.display = 'block';
|
||
document.getElementById('cusetc-textarea').focus();
|
||
btn.style.display = 'none';
|
||
}
|
||
|
||
// 특이사항 저장
|
||
async function saveCusetc(cuscode) {
|
||
const textarea = document.getElementById('cusetc-textarea');
|
||
const newValue = textarea.value.trim();
|
||
|
||
try {
|
||
const res = await fetch(`/api/members/${cuscode}/cusetc`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ cusetc: newValue })
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
// 뷰 업데이트
|
||
const viewEl = document.getElementById('cusetc-view');
|
||
viewEl.innerHTML = newValue || '<span style="color: #9ca3af; font-weight: normal;">없음</span>';
|
||
viewEl.style.maxHeight = newValue.length > 30 ? '40px' : 'none';
|
||
|
||
cancelCusetc();
|
||
alert('✅ 저장되었습니다.');
|
||
} else {
|
||
alert('❌ ' + (data.error || '저장 실패'));
|
||
}
|
||
} catch (err) {
|
||
alert('❌ 오류: ' + err.message);
|
||
}
|
||
}
|
||
|
||
// 특이사항 수정 취소
|
||
function cancelCusetc() {
|
||
document.getElementById('cusetc-view').style.display = 'block';
|
||
document.getElementById('cusetc-edit').style.display = 'none';
|
||
// 수정 버튼 다시 표시
|
||
const editBtn = document.querySelector('#cusetc-view').parentElement.querySelector('button');
|
||
if (editBtn) editBtn.style.display = 'inline-block';
|
||
}
|
||
|
||
// 토스트 알림 함수
|
||
function showToast(message, type = 'info') {
|
||
let container = document.querySelector('.toast-container');
|
||
if (!container) {
|
||
container = document.createElement('div');
|
||
container.className = 'toast-container';
|
||
document.body.appendChild(container);
|
||
}
|
||
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
|
||
const icons = { success: '✅', error: '❌', info: 'ℹ️', printing: '🖨️' };
|
||
toast.innerHTML = `<span>${icons[type] || ''}</span><span>${message}</span>`;
|
||
|
||
container.appendChild(toast);
|
||
|
||
setTimeout(() => toast.remove(), 3000);
|
||
}
|
||
|
||
// 인쇄용 전역 변수
|
||
let printData = { name: '', cusetc: '', phone: '' };
|
||
|
||
// 특이사항 인쇄 실행
|
||
async function doPrintCusetc() {
|
||
// 즉시 피드백
|
||
showToast(`${printData.name}님 특이사항 인쇄 중...`, 'printing');
|
||
|
||
try {
|
||
const res = await fetch('/api/print/cusetc', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
customer_name: printData.name,
|
||
cusetc: printData.cusetc,
|
||
phone: printData.phone
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
showToast(data.message, 'success');
|
||
} else {
|
||
showToast('인쇄 실패: ' + (data.error || '알 수 없는 오류'), 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('오류: ' + err.message, 'error');
|
||
}
|
||
}
|
||
|
||
function renderUserDetail(data) {
|
||
// 전역 변수에 데이터 저장
|
||
currentUserData = data;
|
||
|
||
const user = data.user;
|
||
const mileageHistory = data.mileage_history;
|
||
const purchases = data.purchases;
|
||
|
||
let html = `
|
||
<!-- 사용자 기본 정보 -->
|
||
<div style="background: #f8f9fa; border-radius: 12px; padding: 20px; margin-bottom: 24px;">
|
||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-bottom: 16px;">
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">이름</div>
|
||
<div style="color: #212529; font-size: 16px; font-weight: 600;">
|
||
${user.name}
|
||
${user.is_kakao_verified ? '<span style="display: inline-flex; align-items: center; gap: 3px; background: #FEE500; color: #3C1E1E; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; margin-left: 8px;"><span style="font-size: 13px;">💬</span>카카오</span>' : '<span style="display: inline-flex; align-items: center; gap: 3px; background: #e9ecef; color: #868e96; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px; margin-left: 8px;">미인증</span>'}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">전화번호</div>
|
||
<div style="color: #212529; font-size: 16px; font-weight: 600; font-family: 'Courier New', monospace;">${user.phone}</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">포인트 잔액</div>
|
||
<div id="user-balance-display" style="color: #6366f1; font-size: 18px; font-weight: 700;">${user.balance.toLocaleString()}P</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">가입일</div>
|
||
<div style="color: #212529; font-size: 16px; font-weight: 600;">${user.created_at}</div>
|
||
</div>
|
||
${user.birthday ? `
|
||
<div>
|
||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">🎂 생일</div>
|
||
<div style="color: #ec4899; font-size: 16px; font-weight: 600;">${user.birthday.includes('-') ? user.birthday.split('-')[0] + '월 ' + user.birthday.split('-')[1] + '일' : user.birthday.slice(0,2) + '월 ' + user.birthday.slice(2,4) + '일'}</div>
|
||
</div>
|
||
` : ''}
|
||
<!-- 특이(참고)사항 - 생일 옆 칸 -->
|
||
${data.pos_customer ? `
|
||
<div>
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
|
||
<span style="color: #d97706; font-size: 13px;">⚠️ 특이사항</span>
|
||
<button onclick="editCusetc('${data.pos_customer.cuscode}', this)" style="background: none; border: 1px solid #d97706; color: #d97706; font-size: 11px; padding: 2px 8px; border-radius: 4px; cursor: pointer;">✏️ 수정</button>
|
||
${data.pos_customer.cusetc ? `<button onclick="printData={name:'${data.pos_customer.name}',cusetc:decodeURIComponent('${encodeURIComponent(data.pos_customer.cusetc)}'),phone:'${user.phone||''}'};doPrintCusetc()" style="background: none; border: 1px solid #6b7280; color: #6b7280; font-size: 11px; padding: 2px 8px; border-radius: 4px; cursor: pointer;">🖨️ 인쇄</button>` : ''}
|
||
</div>
|
||
<div id="cusetc-view" onclick="toggleCusetc(this)" style="color: #92400e; font-size: 14px; font-weight: 500; cursor: ${(data.pos_customer.cusetc || '').length > 30 ? 'pointer' : 'default'}; ${(data.pos_customer.cusetc || '').length > 30 ? 'max-height: 40px; overflow: hidden;' : ''}" title="${(data.pos_customer.cusetc || '').length > 30 ? '클릭하여 펼치기' : ''}">
|
||
${data.pos_customer.cusetc || '<span style="color: #9ca3af; font-weight: normal;">없음</span>'}
|
||
</div>
|
||
<div id="cusetc-edit" style="display: none;">
|
||
<textarea id="cusetc-textarea" style="width: 100%; min-height: 60px; padding: 8px; border: 1px solid #d97706; border-radius: 6px; font-size: 13px; resize: vertical;">${data.pos_customer.cusetc || ''}</textarea>
|
||
<div style="display: flex; gap: 6px; margin-top: 6px;">
|
||
<button onclick="saveCusetc('${data.pos_customer.cuscode}')" style="background: #d97706; color: white; border: none; padding: 4px 12px; border-radius: 4px; font-size: 12px; cursor: pointer;">저장</button>
|
||
<button onclick="cancelCusetc()" style="background: #e5e7eb; color: #374151; border: none; padding: 4px 12px; border-radius: 4px; font-size: 12px; cursor: pointer;">취소</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
<div style="text-align: right; display: flex; gap: 8px; justify-content: flex-end;">
|
||
<button onclick="showAIAnalysisModal(${user.id})" style="padding: 10px 24px; background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%); color: white; border: none; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s;">
|
||
🤖 AI 분석
|
||
</button>
|
||
<button onclick="showUsePointsModal(${user.id}, ${user.balance})" style="padding: 10px 24px; background: #f03e3e; color: white; border: none; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s;">
|
||
💳 포인트 사용
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 탭 메뉴 -->
|
||
<div style="display: flex; gap: 16px; margin-bottom: 16px; border-bottom: 2px solid #e9ecef;">
|
||
<button onclick="switchTab('purchases')" id="tab-purchases" class="tab-btn" style="padding: 12px 20px; border: none; background: none; font-size: 15px; font-weight: 600; cursor: pointer; border-bottom: 3px solid #6366f1; color: #6366f1;">
|
||
🛒 구매 (${purchases.length})
|
||
</button>
|
||
<button onclick="switchTab('mileage')" id="tab-mileage" class="tab-btn" style="padding: 12px 20px; border: none; background: none; font-size: 15px; font-weight: 600; cursor: pointer; border-bottom: 3px solid transparent; color: #868e96;">
|
||
💰 적립 (${mileageHistory.length})
|
||
</button>
|
||
<button onclick="switchTab('prescriptions')" id="tab-prescriptions" class="tab-btn" style="padding: 12px 20px; border: none; background: none; font-size: 15px; font-weight: 600; cursor: pointer; border-bottom: 3px solid transparent; color: #868e96;">
|
||
💊 조제 (${data.prescriptions ? data.prescriptions.length : 0})
|
||
</button>
|
||
<button onclick="switchTab('interests')" id="tab-interests" class="tab-btn" style="padding: 12px 20px; border: none; background: none; font-size: 15px; font-weight: 600; cursor: pointer; border-bottom: 3px solid transparent; color: #868e96;">
|
||
💝 관심 (${data.interests ? data.interests.length : 0})
|
||
</button>
|
||
<button onclick="switchTab('pets')" id="tab-pets" class="tab-btn" style="padding: 12px 20px; border: none; background: none; font-size: 15px; font-weight: 600; cursor: pointer; border-bottom: 3px solid transparent; color: #868e96;">
|
||
🐾 반려동물 (${data.pets ? data.pets.length : 0})
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 정렬 버튼 (구매 이력용) -->
|
||
<div id="sort-buttons" style="display: flex; gap: 8px; margin-bottom: 16px; justify-content: flex-end;">
|
||
<button onclick="sortPurchases('date')" id="sort-date" class="sort-btn active">
|
||
📅 날짜별
|
||
</button>
|
||
<button onclick="sortPurchases('amount')" id="sort-amount" class="sort-btn">
|
||
💰 금액별
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 구매 이력 탭 -->
|
||
<div id="tab-content-purchases" class="tab-content">
|
||
<div id="purchase-history-container"></div>
|
||
</div>
|
||
|
||
<!-- 적립 이력 탭 -->
|
||
<div id="tab-content-mileage" class="tab-content" style="display: none;">
|
||
`;
|
||
|
||
// 적립 이력 테이블
|
||
if (mileageHistory.length > 0) {
|
||
html += `
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<thead>
|
||
<tr style="background: #f8f9fa; border-bottom: 2px solid #e9ecef;">
|
||
<th style="padding: 12px; text-align: left; font-size: 13px; color: #495057; font-weight: 600;">일시</th>
|
||
<th style="padding: 12px; text-align: left; font-size: 13px; color: #495057; font-weight: 600;">내용</th>
|
||
<th style="padding: 12px; text-align: right; font-size: 13px; color: #495057; font-weight: 600;">포인트</th>
|
||
<th style="padding: 12px; text-align: right; font-size: 13px; color: #495057; font-weight: 600;">잔액</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
mileageHistory.forEach(ml => {
|
||
html += `
|
||
<tr style="border-bottom: 1px solid #f1f3f5;">
|
||
<td style="padding: 14px; font-size: 14px; color: #495057;">${ml.created_at}</td>
|
||
<td style="padding: 14px; font-size: 14px; color: #212529; font-weight: 500;">${ml.description || ml.reason}</td>
|
||
<td style="padding: 14px; text-align: right; font-size: 14px; color: #6366f1; font-weight: 600;">${ml.points >= 0 ? '+' : ''}${ml.points.toLocaleString()}P</td>
|
||
<td style="padding: 14px; text-align: right; font-size: 14px; color: #495057;">${ml.balance_after.toLocaleString()}P</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
} else {
|
||
html += '<p style="text-align: center; padding: 40px; color: #868e96;">적립 이력이 없습니다.</p>';
|
||
}
|
||
|
||
html += `
|
||
</div>
|
||
|
||
<!-- 조제 이력 탭 -->
|
||
<div id="tab-content-prescriptions" class="tab-content" style="display: none;">
|
||
`;
|
||
|
||
// 조제 이력 렌더링
|
||
const prescriptions = data.prescriptions || [];
|
||
if (prescriptions.length > 0) {
|
||
prescriptions.forEach(rx => {
|
||
// 날짜 포맷
|
||
const dateStr = rx.date || '';
|
||
let formattedDate = dateStr;
|
||
if (dateStr.length === 8) {
|
||
formattedDate = `${dateStr.slice(0,4)}.${dateStr.slice(4,6)}.${dateStr.slice(6,8)}`;
|
||
}
|
||
|
||
// 처방 품목
|
||
const itemsHtml = (rx.items || []).map(item => {
|
||
const dosage = item.quantity || 1;
|
||
const freq = item.times_per_day || 1;
|
||
const days = item.days || 0;
|
||
return `
|
||
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f1f3f5;">
|
||
<span style="color: #495057; font-size: 14px;">${item.name}</span>
|
||
<span style="color: #6366f1; font-size: 13px; font-weight: 600;">${dosage}정 × ${freq}회 × ${days}일</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
// 약품 코드 배열 (상호작용 체크용)
|
||
const drugCodes = (rx.items || []).map(item => item.drug_code).filter(c => c);
|
||
const drugCodesJson = JSON.stringify(drugCodes).replace(/"/g, '"');
|
||
|
||
html += `
|
||
<div style="border: 1px solid #e9ecef; border-radius: 12px; margin-bottom: 12px; padding: 16px; border-left: 4px solid #6366f1;">
|
||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
||
<span style="font-size: 15px; font-weight: 600; color: #212529;">📅 ${formattedDate}</span>
|
||
<span style="font-size: 13px; color: #6366f1; font-weight: 600;">${rx.total_days || ''}일분</span>
|
||
</div>
|
||
<div style="font-size: 13px; color: #64748b; margin-bottom: 12px;">
|
||
🏥 ${rx.hospital || ''} · ${rx.doctor || ''}
|
||
</div>
|
||
${rx.items && rx.items.length > 0 ? `<div style="background: #f8f9fa; border-radius: 8px; padding: 12px;">${itemsHtml}</div>` : ''}
|
||
${drugCodes.length >= 2 ? `
|
||
<div style="margin-top: 12px; text-align: right;">
|
||
<button onclick='checkDrugInteraction(${drugCodesJson}, "${rx.pre_serial || ""}")'
|
||
style="background: linear-gradient(135deg, #8b5cf6, #6366f1); color: #fff; border: none; padding: 8px 14px; border-radius: 8px; font-size: 12px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px;">
|
||
🔬 AI 상호작용 체크
|
||
</button>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
} else if (!data.pos_customer) {
|
||
html += '<p style="text-align: center; padding: 40px; color: #868e96;">📭 POS 회원으로 등록되지 않았습니다<br><small>전화번호가 POS에 등록되면 조제 이력이 표시됩니다</small></p>';
|
||
} else {
|
||
html += '<p style="text-align: center; padding: 40px; color: #868e96;">📭 조제 이력이 없습니다</p>';
|
||
}
|
||
|
||
html += `
|
||
</div>
|
||
|
||
<!-- 관심상품 탭 -->
|
||
<div id="tab-content-interests" class="tab-content" style="display: none;">
|
||
`;
|
||
|
||
// 관심상품 렌더링
|
||
const interests = data.interests || [];
|
||
if (interests.length > 0) {
|
||
interests.forEach(item => {
|
||
// 날짜 포맷
|
||
const date = item.created_at || '';
|
||
|
||
// 트리거 상품 파싱
|
||
let triggerText = '';
|
||
try {
|
||
const triggers = JSON.parse(item.trigger_products || '[]');
|
||
if (triggers.length > 0) {
|
||
triggerText = triggers.join(', ');
|
||
}
|
||
} catch(e) {}
|
||
|
||
html += `
|
||
<div style="border: 1px solid #e9ecef; border-radius: 12px; margin-bottom: 12px; padding: 16px; border-left: 4px solid #ec4899;">
|
||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
||
<span style="font-size: 15px; font-weight: 700; color: #ec4899;">💝 ${item.product}</span>
|
||
<span style="font-size: 12px; color: #868e96;">${date}</span>
|
||
</div>
|
||
<div style="font-size: 13px; color: #64748b; margin-bottom: 8px;">
|
||
${item.reason || ''}
|
||
</div>
|
||
${triggerText ? `<div style="font-size: 12px; color: #94a3b8; background: #f8f9fa; padding: 8px 12px; border-radius: 6px;">🛒 구매: ${triggerText}</div>` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
} else {
|
||
html += '<p style="text-align: center; padding: 40px; color: #868e96;">💝 관심 상품이 없습니다<br><small>마일리지 적립 시 AI 추천에서 "관심있어요"를 누르면 여기에 표시됩니다</small></p>';
|
||
}
|
||
|
||
html += `
|
||
</div>
|
||
|
||
<!-- 반려동물 탭 -->
|
||
<div id="tab-content-pets" class="tab-content" style="display: none;">
|
||
`;
|
||
|
||
// 반려동물 렌더링
|
||
const pets = data.pets || [];
|
||
if (pets.length > 0) {
|
||
html += '<div style="display: grid; gap: 16px;">';
|
||
pets.forEach(pet => {
|
||
const photoHtml = pet.photo_url
|
||
? `<img src="${pet.photo_url}" alt="${pet.name}" style="width: 80px; height: 80px; border-radius: 50%; object-fit: cover;">`
|
||
: `<div style="width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, #fbbf24, #f59e0b); display: flex; align-items: center; justify-content: center; font-size: 36px;">${pet.species === 'dog' ? '🐕' : (pet.species === 'cat' ? '🐈' : '🐾')}</div>`;
|
||
|
||
html += `
|
||
<div style="border: 1px solid #e9ecef; border-radius: 16px; padding: 20px; display: flex; gap: 20px; align-items: center; border-left: 4px solid #f59e0b;">
|
||
<div style="flex-shrink: 0;">
|
||
${photoHtml}
|
||
</div>
|
||
<div style="flex: 1;">
|
||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
|
||
<span style="font-size: 20px; font-weight: 700; color: #212529;">${pet.name}</span>
|
||
<span style="background: ${pet.species === 'dog' ? '#dbeafe' : '#fce7f3'}; color: ${pet.species === 'dog' ? '#1e40af' : '#9d174d'}; font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 20px;">
|
||
${pet.species_label}
|
||
</span>
|
||
</div>
|
||
<div style="display: flex; flex-wrap: wrap; gap: 12px; font-size: 14px; color: #6b7280;">
|
||
${pet.breed ? `<span>🏷️ ${pet.breed}</span>` : ''}
|
||
${pet.gender_label ? `<span>${pet.gender_label}</span>` : ''}
|
||
${pet.weight ? `<span>⚖️ ${pet.weight}kg</span>` : ''}
|
||
${pet.age_months ? `<span>🎂 ${pet.age_months}개월</span>` : ''}
|
||
</div>
|
||
${pet.notes ? `<div style="margin-top: 8px; font-size: 13px; color: #9ca3af; background: #f9fafb; padding: 8px 12px; border-radius: 8px;">📝 ${pet.notes}</div>` : ''}
|
||
<div style="margin-top: 10px; font-size: 12px; color: #d1d5db;">
|
||
등록일: ${pet.created_at}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
html += '</div>';
|
||
} else {
|
||
html += '<p style="text-align: center; padding: 40px; color: #868e96;">🐾 등록된 반려동물이 없습니다<br><small>고객이 마이페이지에서 반려동물을 등록하면 여기에 표시됩니다</small></p>';
|
||
}
|
||
|
||
html += `
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('userDetailContent').innerHTML = html;
|
||
|
||
// 구매 이력 렌더링 (정렬 적용)
|
||
renderPurchaseHistory();
|
||
}
|
||
|
||
function renderPurchaseHistory() {
|
||
if (!currentUserData) return;
|
||
|
||
const purchases = [...currentUserData.purchases]; // 복사본 생성
|
||
|
||
// 정렬 적용
|
||
if (currentSortType === 'date') {
|
||
// 날짜별 정렬 (최신순)
|
||
purchases.sort((a, b) => {
|
||
const dateA = new Date(a.date);
|
||
const dateB = new Date(b.date);
|
||
return dateB - dateA;
|
||
});
|
||
} else if (currentSortType === 'amount') {
|
||
// 금액별 정렬 (높은 순)
|
||
purchases.sort((a, b) => b.amount - a.amount);
|
||
}
|
||
|
||
// HTML 생성
|
||
let html = '';
|
||
if (purchases.length > 0) {
|
||
purchases.forEach((purchase, index) => {
|
||
const accordionId = `accordion-${index}`;
|
||
html += `
|
||
<div style="border: 1px solid #e9ecef; border-radius: 12px; margin-bottom: 12px; overflow: hidden;">
|
||
<!-- 아코디언 헤더 -->
|
||
<div onclick="toggleAccordion('${accordionId}')" style="padding: 16px; background: #fff; cursor: pointer; display: flex; justify-content: space-between; align-items: center; position: relative;">
|
||
<div style="flex: 1;">
|
||
<div style="font-size: 15px; font-weight: 600; color: #212529; margin-bottom: 6px;">
|
||
${purchase.items_summary}
|
||
${purchase.source === 'pos'
|
||
? '<span style="position: relative; top: -2px; margin-left: 8px; padding: 2px 6px; background: linear-gradient(135deg, #94a3b8, #64748b); color: white; border-radius: 4px; font-size: 10px; font-weight: 600;">POS</span>'
|
||
: '<span style="position: relative; top: -2px; margin-left: 8px; padding: 2px 6px; background: linear-gradient(135deg, #22c55e, #16a34a); color: white; border-radius: 4px; font-size: 10px; font-weight: 600;">QR</span>'}
|
||
</div>
|
||
<div style="font-size: 13px; color: #868e96;">
|
||
${purchase.date} | ${purchase.amount.toLocaleString()}원 구매 | ${purchase.points > 0 ? purchase.points.toLocaleString() + 'P 적립' : '적립 안됨'}
|
||
</div>
|
||
</div>
|
||
<div id="${accordionId}-icon" style="width: 24px; height: 24px; color: #868e96; transition: transform 0.3s;">
|
||
▼
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 아코디언 내용 -->
|
||
<div id="${accordionId}" class="accordion-content" style="max-height: 0; overflow: hidden;">
|
||
<div style="padding: 16px; background: #f8f9fa; border-top: 1px solid #e9ecef;">
|
||
<div style="font-size: 14px; font-weight: 600; color: #495057; margin-bottom: 12px;">
|
||
상품 상세 (${purchase.items_count}개)
|
||
</div>
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<thead>
|
||
<tr style="background: #fff; border-bottom: 1px solid #e9ecef;">
|
||
<th style="padding: 10px; text-align: left; font-size: 12px; color: #868e96; font-weight: 600;">상품코드</th>
|
||
<th style="padding: 10px; text-align: left; font-size: 12px; color: #868e96; font-weight: 600;">상품명</th>
|
||
<th style="padding: 10px; text-align: right; font-size: 12px; color: #868e96; font-weight: 600;">수량</th>
|
||
<th style="padding: 10px; text-align: right; font-size: 12px; color: #868e96; font-weight: 600;">단가</th>
|
||
<th style="padding: 10px; text-align: right; font-size: 12px; color: #868e96; font-weight: 600;">합계</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
purchase.items.forEach(item => {
|
||
// 카테고리 뱃지 생성
|
||
let categoriesBadges = '';
|
||
if (item.categories && item.categories.length > 0) {
|
||
item.categories.forEach(cat => {
|
||
categoriesBadges += `<span class="category-badge cat-${cat.name}">${cat.name}</span>`;
|
||
});
|
||
}
|
||
|
||
html += `
|
||
<tr style="border-bottom: 1px solid #f1f3f5;">
|
||
<td style="padding: 10px; font-size: 13px; color: #495057;">${item.code}</td>
|
||
<td style="padding: 10px; font-size: 13px; color: #212529; font-weight: 500;">
|
||
<div style="margin-bottom: 4px;">${item.name}</div>
|
||
${categoriesBadges ? `<div style="margin-top: 6px;">${categoriesBadges}</div>` : ''}
|
||
</td>
|
||
<td style="padding: 10px; text-align: right; font-size: 13px; color: #495057;">${item.qty}</td>
|
||
<td style="padding: 10px; text-align: right; font-size: 13px; color: #495057;">${item.price.toLocaleString()}원</td>
|
||
<td style="padding: 10px; text-align: right; font-size: 13px; color: #6366f1; font-weight: 600;">${item.total.toLocaleString()}원</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
} else {
|
||
html = '<p style="text-align: center; padding: 40px; color: #868e96;">구매 이력이 없습니다.</p>';
|
||
}
|
||
|
||
document.getElementById('purchase-history-container').innerHTML = html;
|
||
}
|
||
|
||
function sortPurchases(type) {
|
||
currentSortType = type;
|
||
|
||
// 정렬 버튼 스타일 업데이트
|
||
document.querySelectorAll('.sort-btn').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
document.getElementById('sort-' + type).classList.add('active');
|
||
|
||
// 구매 이력 다시 렌더링
|
||
renderPurchaseHistory();
|
||
}
|
||
|
||
function toggleAccordion(accordionId) {
|
||
const content = document.getElementById(accordionId);
|
||
const icon = document.getElementById(accordionId + '-icon');
|
||
|
||
if (content.style.maxHeight && content.style.maxHeight !== '0px') {
|
||
// 닫기
|
||
content.style.maxHeight = '0px';
|
||
icon.style.transform = 'rotate(0deg)';
|
||
} else {
|
||
// 열기
|
||
content.style.maxHeight = content.scrollHeight + 'px';
|
||
icon.style.transform = 'rotate(180deg)';
|
||
}
|
||
}
|
||
|
||
function switchTab(tabName) {
|
||
// 모든 탭 버튼 비활성화
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.style.borderBottom = '3px solid transparent';
|
||
btn.style.color = '#868e96';
|
||
});
|
||
|
||
// 모든 탭 컨텐츠 숨기기
|
||
document.querySelectorAll('.tab-content').forEach(content => {
|
||
content.style.display = 'none';
|
||
});
|
||
|
||
// 선택된 탭 활성화
|
||
document.getElementById('tab-' + tabName).style.borderBottom = '3px solid #6366f1';
|
||
document.getElementById('tab-' + tabName).style.color = '#6366f1';
|
||
document.getElementById('tab-content-' + tabName).style.display = 'block';
|
||
|
||
// 정렬 버튼 표시/숨기기 (구매 이력 탭에만 표시)
|
||
const sortButtons = document.getElementById('sort-buttons');
|
||
if (sortButtons) {
|
||
sortButtons.style.display = (tabName === 'purchases') ? 'flex' : 'none';
|
||
}
|
||
}
|
||
|
||
// ===== 포인트 사용 기능 =====
|
||
|
||
let currentUserId = null;
|
||
let currentUserBalance = 0;
|
||
|
||
function showUsePointsModal(userId, balance) {
|
||
currentUserId = userId;
|
||
currentUserBalance = balance;
|
||
|
||
// 현재 잔액 표시
|
||
document.getElementById('current-balance-display').innerText = balance.toLocaleString() + 'P';
|
||
|
||
// 입력 필드 초기화
|
||
document.getElementById('points-to-use').value = '';
|
||
document.getElementById('use-points-error').style.display = 'none';
|
||
|
||
// 모달 표시
|
||
document.getElementById('usePointsModal').style.display = 'block';
|
||
|
||
// 입력 필드에 포커스
|
||
setTimeout(() => {
|
||
document.getElementById('points-to-use').focus();
|
||
}, 100);
|
||
}
|
||
|
||
function closeUsePointsModal() {
|
||
document.getElementById('usePointsModal').style.display = 'none';
|
||
currentUserId = null;
|
||
currentUserBalance = 0;
|
||
}
|
||
|
||
function confirmUsePoints() {
|
||
const pointsInput = document.getElementById('points-to-use');
|
||
const points = parseInt(pointsInput.value);
|
||
const errorDiv = document.getElementById('use-points-error');
|
||
|
||
// 유효성 검사
|
||
if (!points || points <= 0) {
|
||
errorDiv.innerText = '1 이상의 포인트를 입력하세요.';
|
||
errorDiv.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
if (points > currentUserBalance) {
|
||
errorDiv.innerText = `잔액(${currentUserBalance.toLocaleString()}P)보다 많이 사용할 수 없습니다.`;
|
||
errorDiv.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
// API 호출
|
||
fetch('/admin/use-points', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
user_id: currentUserId,
|
||
points: points
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// 성공 - userId 저장 후 모달 닫고 사용자 상세 정보 새로고침
|
||
const userId = currentUserId; // closeUsePointsModal() 전에 저장
|
||
closeUsePointsModal();
|
||
showUserDetail(userId);
|
||
|
||
// 성공 메시지 표시 (선택사항)
|
||
alert(`${points.toLocaleString()}P 사용 완료!\n남은 잔액: ${data.new_balance.toLocaleString()}P`);
|
||
} else {
|
||
// 실패 - 에러 메시지 표시
|
||
errorDiv.innerText = data.message || '포인트 사용에 실패했습니다.';
|
||
errorDiv.style.display = 'block';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
errorDiv.innerText = '네트워크 오류가 발생했습니다.';
|
||
errorDiv.style.display = 'block';
|
||
});
|
||
}
|
||
|
||
// ESC 키로 모달 닫기
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
// 포인트 사용 모달이 열려있으면 먼저 닫기
|
||
if (document.getElementById('usePointsModal').style.display === 'block') {
|
||
closeUsePointsModal();
|
||
} else {
|
||
closeUserModal();
|
||
}
|
||
}
|
||
});
|
||
|
||
// 사용자 모달 배경 클릭 시 닫기
|
||
document.getElementById('userDetailModal').addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
closeUserModal();
|
||
}
|
||
});
|
||
|
||
// 포인트 사용 모달 배경 클릭 시 닫기
|
||
document.getElementById('usePointsModal').addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
closeUsePointsModal();
|
||
}
|
||
});
|
||
|
||
// ===== 검색 기능 =====
|
||
|
||
let currentSearchType = 'user'; // 'user' 또는 'product'
|
||
|
||
function switchSearchType(type) {
|
||
currentSearchType = type;
|
||
|
||
// 버튼 스타일 변경
|
||
document.getElementById('btn-search-user').classList.toggle('active', type === 'user');
|
||
document.getElementById('btn-search-product').classList.toggle('active', type === 'product');
|
||
|
||
// placeholder 변경
|
||
const searchInput = document.getElementById('searchInput');
|
||
if (type === 'user') {
|
||
searchInput.placeholder = '이름 또는 전화번호 입력';
|
||
} else {
|
||
searchInput.placeholder = '제품명 입력';
|
||
}
|
||
|
||
searchInput.value = '';
|
||
}
|
||
|
||
function performSearch() {
|
||
const query = document.getElementById('searchInput').value.trim();
|
||
|
||
if (!query) {
|
||
alert('검색어를 입력하세요.');
|
||
return;
|
||
}
|
||
|
||
if (currentSearchType === 'user') {
|
||
searchUsers(query);
|
||
} else {
|
||
searchProducts(query);
|
||
}
|
||
}
|
||
|
||
function searchUsers(query) {
|
||
// 전화번호 뒷자리인지 확인 (숫자만 있고 4-7자리)
|
||
const isPhoneLast = /^\d{4,7}$/.test(query);
|
||
const type = isPhoneLast ? 'phone_last' : (query.match(/^\d+$/) ? 'phone' : 'name');
|
||
|
||
fetch(`/admin/search/user?q=${encodeURIComponent(query)}&type=${type}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
if (data.multiple) {
|
||
// 여러 명 매칭 → 선택 모달
|
||
showUserSelectionModal(data.users, query);
|
||
} else if (data.user_id) {
|
||
// 단일 매칭 → 바로 사용자 상세 모달
|
||
showUserDetail(data.user_id);
|
||
} else {
|
||
alert('검색 결과가 없습니다.');
|
||
}
|
||
} else {
|
||
alert(data.message || '검색 실패');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert('검색 중 오류가 발생했습니다.');
|
||
console.error(error);
|
||
});
|
||
}
|
||
|
||
function searchProducts(query) {
|
||
fetch(`/admin/search/product?q=${encodeURIComponent(query)}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showProductSearchResults(data.results, query);
|
||
} else {
|
||
alert(data.message || '검색 실패');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert('검색 중 오류가 발생했습니다.');
|
||
console.error(error);
|
||
});
|
||
}
|
||
|
||
function showUserSelectionModal(users, query) {
|
||
let html = `
|
||
<div style="margin-bottom: 16px; color: #868e96; font-size: 14px;">
|
||
"${query}" 검색 결과: ${users.length}명
|
||
</div>
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<thead>
|
||
<tr style="background: #f8f9fa; border-bottom: 2px solid #e9ecef;">
|
||
<th style="padding: 12px; text-align: left; font-size: 13px; color: #495057; font-weight: 600;">이름</th>
|
||
<th style="padding: 12px; text-align: left; font-size: 13px; color: #495057; font-weight: 600;">전화번호</th>
|
||
<th style="padding: 12px; text-align: right; font-size: 13px; color: #495057; font-weight: 600;">포인트</th>
|
||
<th style="padding: 12px; text-align: center; font-size: 13px; color: #495057; font-weight: 600;">선택</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
users.forEach(user => {
|
||
const kakaoBadge = user.is_kakao_verified
|
||
? '<span style="display: inline-flex; align-items: center; gap: 2px; background: #FEE500; color: #3C1E1E; font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 8px; margin-left: 6px;">💬</span>'
|
||
: '<span style="display: inline-flex; align-items: center; background: #e9ecef; color: #868e96; font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 8px; margin-left: 6px;">미인증</span>';
|
||
html += `
|
||
<tr style="border-bottom: 1px solid #f1f3f5;">
|
||
<td style="padding: 14px; font-size: 14px; color: #212529; font-weight: 500;">${user.name}${kakaoBadge}</td>
|
||
<td style="padding: 14px; font-size: 14px; color: #495057; font-family: 'Courier New', monospace;">${user.phone}</td>
|
||
<td style="padding: 14px; text-align: right; font-size: 14px; color: #6366f1; font-weight: 600;">${user.balance.toLocaleString()}P</td>
|
||
<td style="padding: 14px; text-align: center;">
|
||
<button onclick="selectUser(${user.id})" style="padding: 8px 16px; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: #fff; border: none; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer;">선택</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
document.getElementById('searchResultTitle').textContent = '사용자 선택';
|
||
document.getElementById('searchResultContent').innerHTML = html;
|
||
document.getElementById('searchResultsModal').style.display = 'block';
|
||
}
|
||
|
||
function selectUser(userId) {
|
||
closeSearchResults();
|
||
showUserDetail(userId);
|
||
}
|
||
|
||
function showProductSearchResults(results, query) {
|
||
if (results.length === 0) {
|
||
document.getElementById('searchResultTitle').textContent = '검색 결과';
|
||
document.getElementById('searchResultContent').innerHTML = `
|
||
<div style="text-align: center; padding: 60px; color: #868e96;">
|
||
<div style="font-size: 16px; margin-bottom: 8px;">검색 결과가 없습니다.</div>
|
||
<div style="font-size: 14px;">제품명 "${query}"를 구매하고 적립한 사용자가 없습니다.</div>
|
||
</div>
|
||
`;
|
||
document.getElementById('searchResultsModal').style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
let html = `
|
||
<div style="margin-bottom: 16px; color: #868e96; font-size: 14px;">
|
||
"${query}" 구매 적립자: ${results.length}명
|
||
</div>
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<thead>
|
||
<tr style="background: #f8f9fa; border-bottom: 2px solid #e9ecef;">
|
||
<th style="padding: 12px; text-align: left; font-size: 13px; color: #495057; font-weight: 600;">이름</th>
|
||
<th style="padding: 12px; text-align: left; font-size: 13px; color: #495057; font-weight: 600;">전화번호</th>
|
||
<th style="padding: 12px; text-align: left; font-size: 13px; color: #495057; font-weight: 600;">구매일시</th>
|
||
<th style="padding: 12px; text-align: left; font-size: 13px; color: #495057; font-weight: 600;">적립일시</th>
|
||
<th style="padding: 12px; text-align: right; font-size: 13px; color: #495057; font-weight: 600;">수량</th>
|
||
<th style="padding: 12px; text-align: right; font-size: 13px; color: #495057; font-weight: 600;">구매금액</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
results.forEach(result => {
|
||
html += `
|
||
<tr style="border-bottom: 1px solid #f1f3f5; cursor: pointer;" onclick="selectUser(${result.user_id})">
|
||
<td style="padding: 14px; font-size: 14px; color: #6366f1; font-weight: 600;">${result.user_name}</td>
|
||
<td style="padding: 14px; font-size: 14px; color: #495057; font-family: 'Courier New', monospace;">${result.user_phone}</td>
|
||
<td style="padding: 14px; font-size: 14px; color: #495057;">${result.purchase_date}</td>
|
||
<td style="padding: 14px; font-size: 14px; color: #868e96;">${result.claimed_date}</td>
|
||
<td style="padding: 14px; text-align: right; font-size: 14px; color: #495057;">${result.quantity}</td>
|
||
<td style="padding: 14px; text-align: right; font-size: 14px; color: #6366f1; font-weight: 600;">${result.total_amount.toLocaleString()}원</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
document.getElementById('searchResultTitle').textContent = '제품 구매 적립자';
|
||
document.getElementById('searchResultContent').innerHTML = html;
|
||
document.getElementById('searchResultsModal').style.display = 'block';
|
||
}
|
||
|
||
function closeSearchResults() {
|
||
document.getElementById('searchResultsModal').style.display = 'none';
|
||
}
|
||
|
||
// 검색 결과 모달 배경 클릭 시 닫기
|
||
document.getElementById('searchResultsModal').addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
closeSearchResults();
|
||
}
|
||
});
|
||
|
||
// ESC 키로 검색 결과 모달도 닫기
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
closeSearchResults();
|
||
}
|
||
});
|
||
|
||
// ===== AI 분석 기능 =====
|
||
|
||
let aiAnalysisCache = {}; // 캐싱용
|
||
let lottieAnimation = null; // Lottie 애니메이션 인스턴스
|
||
|
||
function showAIAnalysisModal(userId) {
|
||
// 모달 열기
|
||
document.getElementById('aiAnalysisModal').style.display = 'block';
|
||
|
||
// 캐시 확인 (5분 이내)
|
||
const cacheKey = `ai_analysis_${userId}`;
|
||
const cached = aiAnalysisCache[cacheKey];
|
||
const now = Date.now();
|
||
|
||
if (cached && (now - cached.timestamp) < 300000) {
|
||
renderAIAnalysis(cached.data);
|
||
return;
|
||
}
|
||
|
||
// Lottie 애니메이션 로딩 표시 (로컬)
|
||
document.getElementById('aiAnalysisContent').innerHTML = `
|
||
<div style="text-align: center; padding: 60px;">
|
||
<div id="lottie-animation" style="width: 200px; height: 200px; margin: 0 auto 24px;"></div>
|
||
<div style="font-size: 16px; color: #495057; font-weight: 600; margin-bottom: 8px;">
|
||
AI가 구매 패턴을 분석하고 있습니다...
|
||
</div>
|
||
<div style="font-size: 14px; color: #868e96;">
|
||
최대 10-15초 소요될 수 있습니다
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Lottie 애니메이션 로드
|
||
if (window.lottie) {
|
||
lottieAnimation = lottie.loadAnimation({
|
||
container: document.getElementById('lottie-animation'),
|
||
renderer: 'svg',
|
||
loop: true,
|
||
autoplay: true,
|
||
path: '/static/animations/ai-loading.json'
|
||
});
|
||
}
|
||
|
||
// API 호출
|
||
fetch(`/admin/ai-analyze-user/${userId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// 캐시 저장
|
||
aiAnalysisCache[cacheKey] = {
|
||
data: data,
|
||
timestamp: Date.now()
|
||
};
|
||
renderAIAnalysis(data);
|
||
} else {
|
||
showAIAnalysisError(data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showAIAnalysisError('네트워크 오류가 발생했습니다. 다시 시도해주세요.');
|
||
console.error('AI Analysis Error:', error);
|
||
});
|
||
}
|
||
|
||
function renderAIAnalysis(data) {
|
||
const user = data.user;
|
||
const analysis = data.analysis;
|
||
|
||
let html = `
|
||
<!-- 사용자 정보 헤더 -->
|
||
<div style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-radius: 12px; padding: 16px; margin-bottom: 24px;">
|
||
<div style="font-size: 14px; color: #495057; margin-bottom: 4px;">분석 대상</div>
|
||
<div style="font-size: 18px; font-weight: 700; color: #212529;">
|
||
${user.name} (${user.phone})
|
||
</div>
|
||
<div style="font-size: 13px; color: #868e96; margin-top: 4px;">
|
||
${user.balance.toLocaleString()}P 보유
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 구매 패턴 분석 -->
|
||
<div style="margin-bottom: 24px;">
|
||
<h3 style="font-size: 16px; font-weight: 700; color: #495057; margin-bottom: 12px; display: flex; align-items: center;">
|
||
<span style="margin-right: 8px;">📊</span> 구매 패턴 분석
|
||
</h3>
|
||
<div style="background: #f8f9fa; border-radius: 8px; padding: 16px; font-size: 14px; line-height: 1.8; color: #212529; white-space: pre-line;">
|
||
${analysis.pattern}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 주요 구매 품목 -->
|
||
<div style="margin-bottom: 24px;">
|
||
<h3 style="font-size: 16px; font-weight: 700; color: #495057; margin-bottom: 12px; display: flex; align-items: center;">
|
||
<span style="margin-right: 8px;">💊</span> 주요 구매 품목
|
||
</h3>
|
||
<ul style="list-style: none; padding: 0; margin: 0;">
|
||
`;
|
||
|
||
analysis.main_products.forEach(product => {
|
||
html += `
|
||
<li style="background: #fff; border: 1px solid #e9ecef; border-radius: 8px; padding: 12px; margin-bottom: 8px; font-size: 14px; color: #212529;">
|
||
• ${product}
|
||
</li>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- 추천 제품 -->
|
||
<div style="margin-bottom: 24px;">
|
||
<h3 style="font-size: 16px; font-weight: 700; color: #495057; margin-bottom: 12px; display: flex; align-items: center;">
|
||
<span style="margin-right: 8px;">✨</span> 추천 제품 (업셀링)
|
||
</h3>
|
||
<div style="background: linear-gradient(135deg, #e0f2fe 0%, #ddd6fe 100%); border-radius: 8px; padding: 16px;">
|
||
<ul style="margin: 0; padding-left: 20px; font-size: 14px; line-height: 1.8; color: #212529;">
|
||
`;
|
||
|
||
analysis.recommendations.forEach(rec => {
|
||
html += `<li>${rec}</li>`;
|
||
});
|
||
|
||
html += `
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 마케팅 전략 -->
|
||
<div>
|
||
<h3 style="font-size: 16px; font-weight: 700; color: #495057; margin-bottom: 12px; display: flex; align-items: center;">
|
||
<span style="margin-right: 8px;">🎯</span> 마케팅 전략 제안
|
||
</h3>
|
||
<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 16px; border-radius: 4px; font-size: 14px; line-height: 1.8; color: #856404;">
|
||
${analysis.marketing_strategy}
|
||
</div>
|
||
</div>
|
||
|
||
${data.metadata ? `
|
||
<div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid #e9ecef; font-size: 12px; color: #868e96; text-align: right;">
|
||
분석 모델: ${data.metadata.model_used} | 분석 시간: ${data.metadata.analysis_time}
|
||
</div>
|
||
` : ''}
|
||
`;
|
||
|
||
document.getElementById('aiAnalysisContent').innerHTML = html;
|
||
}
|
||
|
||
function showAIAnalysisError(message) {
|
||
document.getElementById('aiAnalysisContent').innerHTML = `
|
||
<div style="text-align: center; padding: 60px;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">⚠️</div>
|
||
<div style="font-size: 16px; color: #f03e3e; font-weight: 600; margin-bottom: 8px;">
|
||
AI 분석 실패
|
||
</div>
|
||
<div style="font-size: 14px; color: #868e96; margin-bottom: 20px;">
|
||
${message}
|
||
</div>
|
||
<button onclick="closeAIAnalysisModal()" style="padding: 10px 24px; background: #f1f3f5; border: none; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 600;">
|
||
닫기
|
||
</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function closeAIAnalysisModal() {
|
||
document.getElementById('aiAnalysisModal').style.display = 'none';
|
||
if (lottieAnimation) {
|
||
lottieAnimation.destroy();
|
||
lottieAnimation = null;
|
||
}
|
||
}
|
||
|
||
// AI 분석 모달 배경 클릭 시 닫기
|
||
document.getElementById('aiAnalysisModal').addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
closeAIAnalysisModal();
|
||
}
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════
|
||
// KIMS 약물 상호작용 체크
|
||
// ═══════════════════════════════════════════════════
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
async function checkDrugInteraction(drugCodes, preSerial) {
|
||
// drugCodes가 문자열로 넘어올 수 있음
|
||
if (typeof drugCodes === 'string') {
|
||
try { drugCodes = JSON.parse(drugCodes); } catch(e) { return; }
|
||
}
|
||
|
||
// 로딩 모달 표시
|
||
showInteractionModal('loading');
|
||
|
||
try {
|
||
const response = await fetch('/api/kims/interaction-check', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
drug_codes: drugCodes,
|
||
pre_serial: preSerial
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showInteractionModal('result', data);
|
||
} else {
|
||
showInteractionModal('error', data.error || '알 수 없는 오류');
|
||
}
|
||
} catch (err) {
|
||
showInteractionModal('error', '서버 연결 실패: ' + err.message);
|
||
}
|
||
}
|
||
|
||
function showInteractionModal(type, data) {
|
||
let modal = document.getElementById('interactionModal');
|
||
if (!modal) {
|
||
modal = document.createElement('div');
|
||
modal.id = 'interactionModal';
|
||
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
||
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
let content = '';
|
||
|
||
if (type === 'loading') {
|
||
content = `
|
||
<div style="background:#fff;border-radius:16px;padding:40px;text-align:center;max-width:400px;">
|
||
<div style="font-size:48px;margin-bottom:16px;">🔬</div>
|
||
<div style="font-size:18px;font-weight:600;color:#334155;">상호작용 분석 중...</div>
|
||
<div style="font-size:14px;color:#64748b;margin-top:8px;">KIMS 데이터베이스 조회 중</div>
|
||
</div>
|
||
`;
|
||
} else if (type === 'error') {
|
||
content = `
|
||
<div style="background:#fff;border-radius:16px;padding:30px;max-width:400px;">
|
||
<div style="font-size:40px;text-align:center;margin-bottom:16px;">⚠️</div>
|
||
<div style="font-size:16px;font-weight:600;color:#dc2626;text-align:center;">분석 실패</div>
|
||
<div style="font-size:14px;color:#64748b;margin-top:12px;text-align:center;">${escapeHtml(data)}</div>
|
||
<div style="text-align:center;margin-top:20px;">
|
||
<button onclick="document.getElementById('interactionModal').remove()"
|
||
style="background:#6366f1;color:#fff;border:none;padding:10px 24px;border-radius:8px;cursor:pointer;">
|
||
닫기
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (type === 'result') {
|
||
const interactions = data.interactions || [];
|
||
const drugsChecked = data.drugs_checked || [];
|
||
|
||
// 약품 목록 (상호작용 여부에 따른 색상)
|
||
const drugsHtml = drugsChecked.map(d => {
|
||
const hasInteraction = d.has_interaction;
|
||
const bgColor = hasInteraction ? '#fef2f2' : '#f1f5f9';
|
||
const borderColor = hasInteraction ? '#fca5a5' : '#e2e8f0';
|
||
const textColor = hasInteraction ? '#dc2626' : '#334155';
|
||
const icon = hasInteraction ? '⚠️ ' : '';
|
||
return `<span style="display:inline-block;background:${bgColor};border:1px solid ${borderColor};color:${textColor};padding:4px 8px;border-radius:4px;margin:2px;font-size:12px;">${icon}${escapeHtml(d.name.slice(0,20))}</span>`;
|
||
}).join('');
|
||
|
||
// 상호작용 목록
|
||
let interactionsHtml = '';
|
||
if (interactions.length === 0) {
|
||
interactionsHtml = `
|
||
<div style="text-align:center;padding:30px;">
|
||
<div style="font-size:48px;margin-bottom:12px;">✅</div>
|
||
<div style="font-size:16px;font-weight:600;color:#10b981;">상호작용 없음</div>
|
||
<div style="font-size:13px;color:#64748b;margin-top:8px;">
|
||
${data.total_pairs}개 약품 조합을 검사했습니다.<br>
|
||
주의가 필요한 상호작용이 발견되지 않았습니다.
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
interactionsHtml = interactions.map(item => `
|
||
<div style="background:#fff;border:1px solid ${item.severity_color};border-radius:12px;padding:16px;margin-bottom:12px;">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||
<span style="font-weight:600;color:#334155;">
|
||
${escapeHtml(item.drug1_name?.slice(0,20) || '')} ↔ ${escapeHtml(item.drug2_name?.slice(0,20) || '')}
|
||
</span>
|
||
<span style="background:${item.severity_color};color:#fff;padding:4px 10px;border-radius:12px;font-size:12px;font-weight:500;">
|
||
${item.severity_text}
|
||
</span>
|
||
</div>
|
||
${item.description ? `
|
||
<div style="font-size:13px;color:#475569;margin-bottom:8px;line-height:1.5;">
|
||
📋 ${escapeHtml(item.description)}
|
||
</div>
|
||
` : ''}
|
||
${item.management ? `
|
||
<div style="font-size:12px;color:#059669;background:#ecfdf5;padding:8px 12px;border-radius:6px;">
|
||
💡 ${escapeHtml(item.management.slice(0, 150))}...
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
content = `
|
||
<div style="background:#f8fafc;border-radius:20px;max-width:500px;max-height:80vh;overflow:hidden;display:flex;flex-direction:column;">
|
||
<div style="background:linear-gradient(135deg,#8b5cf6,#6366f1);padding:20px 24px;color:#fff;">
|
||
<div style="font-size:18px;font-weight:700;display:flex;align-items:center;gap:10px;">
|
||
🔬 약물 상호작용 분석
|
||
</div>
|
||
<div style="font-size:13px;opacity:0.9;margin-top:6px;">
|
||
${drugsChecked.length}개 약품 · ${data.total_pairs}개 조합 검사
|
||
</div>
|
||
</div>
|
||
<div style="padding:16px 20px;border-bottom:1px solid #e2e8f0;">
|
||
<div style="font-size:12px;color:#64748b;margin-bottom:6px;">분석 약품</div>
|
||
${drugsHtml}
|
||
</div>
|
||
<div style="flex:1;overflow-y:auto;padding:16px 20px;">
|
||
${interactions.length > 0 ? `
|
||
<div style="font-size:13px;color:#dc2626;font-weight:600;margin-bottom:12px;">
|
||
⚠️ ${interactions.length}건의 상호작용 발견
|
||
</div>
|
||
` : ''}
|
||
${interactionsHtml}
|
||
</div>
|
||
<div style="padding:16px 20px;border-top:1px solid #e2e8f0;text-align:center;">
|
||
<button onclick="document.getElementById('interactionModal').remove()"
|
||
style="background:#6366f1;color:#fff;border:none;padding:12px 32px;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;">
|
||
닫기
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
modal.innerHTML = content;
|
||
}
|
||
</script>
|
||
|
||
<!-- Lottie 애니메이션 라이브러리 (로컬) -->
|
||
<script src="/static/js/lottie.min.js"></script>
|
||
</body>
|
||
</html>
|