feat: 구매이력 정렬 기능 및 상품검색 시간 표시 개선

- 관리자 페이지 사용자 상세 모달에 날짜별/금액별 정렬 버튼 추가
- 구매 이력 정렬 기능 구현 (날짜순/금액순)
- 상품 검색 결과에 구매일시/적립일시 모두 표시

주요 변경사항:

1. 구매 이력 정렬 기능 (admin.html)
   - 날짜별 정렬: 최신순 정렬
   - 금액별 정렬: 구매금액 높은 순 정렬
   - 정렬 버튼 UI: 우측 정렬, 토글 방식
   - 탭 전환 시 정렬 버튼 자동 표시/숨김

2. 상품 검색 시간 표시 개선 (app.py, admin.html)
   - 구매일시: MSSQL InsertTime (실제 거래 시간)
   - 적립일시: SQLite claimed_at (QR 적립 시간)
   - 두 시간 모두 테이블에 표시 (구분 명확화)

3. UI/UX 개선
   - 정렬 버튼 스타일: search-type-btn과 동일한 패턴
   - 적립일시: 회색(#868e96)으로 구매일시와 시각적 구분
   - 정렬 상태 유지: 버튼 클릭 시 active 클래스 토글

기술 구현:
- renderPurchaseHistory() 함수로 구매 이력 동적 렌더링
- sortPurchases(type) 함수로 정렬 로직 처리
- 전역 변수로 현재 사용자 데이터 및 정렬 타입 관리
- JavaScript 배열 복사 후 정렬하여 원본 데이터 보존

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
시골약사 2026-01-23 22:06:47 +09:00
parent 59a33cc249
commit d715b630fe
2 changed files with 133 additions and 46 deletions

View File

@ -807,7 +807,8 @@ def admin_search_product():
'user_id': claim_row['claimed_by_user_id'],
'user_name': claim_row['nickname'],
'user_phone': claim_row['phone'],
'purchase_date': claim_row['claimed_at'][:16].replace('T', ' ') if claim_row['claimed_at'] else '-',
'purchase_date': str(mssql_row.InsertTime)[:16].replace('T', ' ') if mssql_row.InsertTime else '-', # MSSQL 실제 거래 시간
'claimed_date': str(claim_row['claimed_at'])[:16].replace('T', ' ') if claim_row['claimed_at'] else '-', # 적립 시간
'quantity': float(mssql_row.SL_NM_item or 0),
'total_amount': int(claim_row['total_amount'])
})

View File

@ -254,6 +254,28 @@
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;
}
@ -709,7 +731,14 @@
// ===== 사용자 상세 모달 함수 =====
// 전역 변수: 현재 사용자 데이터 저장
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>';
@ -742,6 +771,9 @@
}
function renderUserDetail(data) {
// 전역 변수에 데이터 저장
currentUserData = data;
const user = data.user;
const mileageHistory = data.mileage_history;
const purchases = data.purchases;
@ -784,11 +816,89 @@
</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}`;
@ -849,55 +959,23 @@
`;
});
} else {
html += '<p style="text-align: center; padding: 40px; color: #868e96;">구매 이력이 없습니다.</p>';
html = '<p style="text-align: center; padding: 40px; color: #868e96;">구매 이력이 없습니다.</p>';
}
html += `
</div>
document.getElementById('purchase-history-container').innerHTML = html;
}
<!-- 적립 이력 탭 -->
<div id="tab-content-mileage" class="tab-content" style="display: none;">
`;
function sortPurchases(type) {
currentSortType = type;
// 적립 이력 테이블
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>
`;
// 정렬 버튼 스타일 업데이트
document.querySelectorAll('.sort-btn').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById('sort-' + type).classList.add('active');
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 toggleAccordion(accordionId) {
@ -931,6 +1009,12 @@
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';
}
}
// ===== 포인트 사용 기능 =====
@ -1190,6 +1274,7 @@
<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>
@ -1203,6 +1288,7 @@
<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>