feat: 관리자 페이지 사이드바 및 검색 기능 추가

- 왼쪽 사이드바 추가 (280px, 검색 UI 포함)
- 사용자 검색: 이름/전화번호/전화번호 뒷자리 검색
- 제품 검색: SQLite 적립자 기준으로 구매자 목록 표시
- 다중 매칭 시 선택 모달 표시
- 검색 결과 클릭 시 사용자 상세 모달 연동
- 모바일 반응형 (768px 이하 사이드바 숨김)

API 엔드포인트:
- GET /admin/search/user?q={검색어}&type={name|phone|phone_last}
- GET /admin/search/product?q={제품명}

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 21:19:35 +09:00
parent a652d54ad3
commit 7627efbdfb
2 changed files with 523 additions and 1 deletions

View File

@@ -201,6 +201,135 @@
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;
}
.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>
@@ -211,7 +340,30 @@
</div>
</div>
<div class="container">
<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">
@@ -350,6 +502,22 @@
{% 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>
<!-- 거래 세부 내역 모달 -->
@@ -744,6 +912,202 @@
closeUserModal();
}
});
// ===== 검색 기능 =====
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: 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; 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();
}
});
</script>
</body>
</html>