pharmacy-pos-qr-system/backend/templates/admin.html
시골약사 a652d54ad3 feat: 관리자 페이지 사용자 상세 이력 조회 기능 추가
- Flask 백엔드에 /admin/user/<user_id> API 엔드포인트 추가
- SQLite에서 사용자 정보, 마일리지 이력, 구매 이력 조회
- MSSQL에서 각 거래별 상품 상세 조회 (SALE_SUB + CD_GOODS JOIN)
- "첫번째상품명 외 N개" 형식 요약 생성

- admin.html 사용자 테이블에 클릭 이벤트 추가
- 사용자 상세 모달 UI 구현 (탭 + 아코디언)
- 탭: 구매 이력 / 적립 이력 분리 표시
- 아코디언: 각 구매 건 클릭 시 상품 목록 펼침/접기
- CSS 스타일 추가 (아코디언 애니메이션, 테이블 호버 효과)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-23 19:27:10 +09:00

750 lines
34 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);
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<div class="header-title">📊 관리자 대시보드</div>
<div class="header-subtitle">청춘약국 마일리지 관리</div>
</div>
</div>
<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>
<!-- 거래 세부 내역 모달 -->
<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>
<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();
}
});
// ===== 사용자 상세 모달 함수 =====
function showUserDetail(userId) {
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) {
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;">
<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 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>
<!-- 탭 메뉴 -->
<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="tab-content-purchases" class="tab-content">
`;
// 구매 이력 (아코디언)
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>';
}
html += `
</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;
}
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';
}
// ESC 키로 사용자 모달 닫기
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeUserModal();
}
});
// 사용자 모달 배경 클릭 시 닫기
document.getElementById('userDetailModal').addEventListener('click', function(e) {
if (e.target === this) {
closeUserModal();
}
});
</script>
</body>
</html>