pharmacy-pos-qr-system/backend/templates/admin.html
시골약사 a3252f7f17 feat: Lottie 애니메이션 라이브러리 로컬 통합
- lottie-web 라이브러리를 로컬에 다운로드 (CDN 차단 문제 해결)
- AI 분석 로딩 애니메이션을 커스텀 JSON 파일로 변경
- 외부 CDN 의존성 제거로 안정성 향상

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-23 22:30:16 +09:00

1537 lines
68 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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;
}
.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);
}
/* 사이드바 레이아웃 */
.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;
}
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<div class="header-title">📊 관리자 대시보드</div>
<div class="header-subtitle">청춘약국 마일리지 관리</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>
<!-- 최근 가입 사용자 -->
<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>
</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>
</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>
<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 }}</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="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/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 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}</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>
</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>
</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>
`;
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;">
<div style="flex: 1;">
<div style="font-size: 15px; font-weight: 600; color: #212529; margin-bottom: 6px;">
${purchase.items_summary}
</div>
<div style="font-size: 13px; color: #868e96;">
${purchase.date} | ${purchase.amount.toLocaleString()}원 구매 | ${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 => {
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;">${item.name}</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 => {
html += `
<tr style="border-bottom: 1px solid #f1f3f5;">
<td style="padding: 14px; font-size: 14px; color: #212529; font-weight: 500;">${user.name}</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();
}
});
</script>
<!-- Lottie 애니메이션 라이브러리 (로컬) -->
<script src="/static/js/lottie.min.js"></script>
</body>
</html>