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_id': claim_row['claimed_by_user_id'],
|
||||||
'user_name': claim_row['nickname'],
|
'user_name': claim_row['nickname'],
|
||||||
'user_phone': claim_row['phone'],
|
'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),
|
'quantity': float(mssql_row.SL_NM_item or 0),
|
||||||
'total_amount': int(claim_row['total_amount'])
|
'total_amount': int(claim_row['total_amount'])
|
||||||
})
|
})
|
||||||
|
|||||||
@ -254,6 +254,28 @@
|
|||||||
color: #6366f1;
|
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 {
|
.search-input-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -709,7 +731,14 @@
|
|||||||
|
|
||||||
// ===== 사용자 상세 모달 함수 =====
|
// ===== 사용자 상세 모달 함수 =====
|
||||||
|
|
||||||
|
// 전역 변수: 현재 사용자 데이터 저장
|
||||||
|
let currentUserData = null;
|
||||||
|
let currentSortType = 'date'; // 'date' 또는 'amount'
|
||||||
|
|
||||||
function showUserDetail(userId) {
|
function showUserDetail(userId) {
|
||||||
|
// 정렬 타입 초기화
|
||||||
|
currentSortType = 'date';
|
||||||
|
|
||||||
document.getElementById('userDetailModal').style.display = 'block';
|
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>';
|
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) {
|
function renderUserDetail(data) {
|
||||||
|
// 전역 변수에 데이터 저장
|
||||||
|
currentUserData = data;
|
||||||
|
|
||||||
const user = data.user;
|
const user = data.user;
|
||||||
const mileageHistory = data.mileage_history;
|
const mileageHistory = data.mileage_history;
|
||||||
const purchases = data.purchases;
|
const purchases = data.purchases;
|
||||||
@ -784,11 +816,89 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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="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) {
|
if (purchases.length > 0) {
|
||||||
purchases.forEach((purchase, index) => {
|
purchases.forEach((purchase, index) => {
|
||||||
const accordionId = `accordion-${index}`;
|
const accordionId = `accordion-${index}`;
|
||||||
@ -849,55 +959,23 @@
|
|||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
html += '<p style="text-align: center; padding: 40px; color: #868e96;">구매 이력이 없습니다.</p>';
|
html = '<p style="text-align: center; padding: 40px; color: #868e96;">구매 이력이 없습니다.</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `
|
document.getElementById('purchase-history-container').innerHTML = html;
|
||||||
</div>
|
}
|
||||||
|
|
||||||
<!-- 적립 이력 탭 -->
|
function sortPurchases(type) {
|
||||||
<div id="tab-content-mileage" class="tab-content" style="display: none;">
|
currentSortType = type;
|
||||||
`;
|
|
||||||
|
|
||||||
// 적립 이력 테이블
|
// 정렬 버튼 스타일 업데이트
|
||||||
if (mileageHistory.length > 0) {
|
document.querySelectorAll('.sort-btn').forEach(btn => {
|
||||||
html += `
|
btn.classList.remove('active');
|
||||||
<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>
|
|
||||||
`;
|
|
||||||
});
|
});
|
||||||
|
document.getElementById('sort-' + type).classList.add('active');
|
||||||
|
|
||||||
html += `
|
// 구매 이력 다시 렌더링
|
||||||
</tbody>
|
renderPurchaseHistory();
|
||||||
</table>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
html += '<p style="text-align: center; padding: 40px; color: #868e96;">적립 이력이 없습니다.</p>';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('userDetailContent').innerHTML = html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAccordion(accordionId) {
|
function toggleAccordion(accordionId) {
|
||||||
@ -931,6 +1009,12 @@
|
|||||||
document.getElementById('tab-' + tabName).style.borderBottom = '3px solid #6366f1';
|
document.getElementById('tab-' + tabName).style.borderBottom = '3px solid #6366f1';
|
||||||
document.getElementById('tab-' + tabName).style.color = '#6366f1';
|
document.getElementById('tab-' + tabName).style.color = '#6366f1';
|
||||||
document.getElementById('tab-content-' + tabName).style.display = 'block';
|
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: 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>
|
||||||
<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>
|
</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: #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; 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: #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: #495057;">${result.quantity}</td>
|
||||||
<td style="padding: 14px; text-align: right; font-size: 14px; color: #6366f1; font-weight: 600;">${result.total_amount.toLocaleString()}원</td>
|
<td style="padding: 14px; text-align: right; font-size: 14px; color: #6366f1; font-weight: 600;">${result.total_amount.toLocaleString()}원</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user