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:
parent
59a33cc249
commit
d715b630fe
@ -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'])
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user