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>
This commit is contained in:
parent
622a143e19
commit
a652d54ad3
139
backend/app.py
139
backend/app.py
@ -456,6 +456,145 @@ def admin_transaction_detail(transaction_id):
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/admin/user/<int:user_id>')
|
||||||
|
def admin_user_detail(user_id):
|
||||||
|
"""사용자 상세 이력 조회 - 구매 이력, 적립 이력, 구매 품목"""
|
||||||
|
try:
|
||||||
|
# 1. SQLite 연결
|
||||||
|
conn = db_manager.get_sqlite_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 2. 사용자 기본 정보 조회
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, nickname, phone, mileage_balance, created_at
|
||||||
|
FROM users WHERE id = ?
|
||||||
|
""", (user_id,))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '사용자를 찾을 수 없습니다.'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
# 3. 마일리지 이력 조회 (최근 50건)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT transaction_id, points, balance_after, reason, description, created_at
|
||||||
|
FROM mileage_ledger
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 50
|
||||||
|
""", (user_id,))
|
||||||
|
mileage_history = cursor.fetchall()
|
||||||
|
|
||||||
|
# 4. 구매 이력 조회 (적립된 거래만, 최근 20건)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT transaction_id, total_amount, claimable_points, claimed_at
|
||||||
|
FROM claim_tokens
|
||||||
|
WHERE claimed_by_user_id = ?
|
||||||
|
ORDER BY claimed_at DESC
|
||||||
|
LIMIT 20
|
||||||
|
""", (user_id,))
|
||||||
|
claimed_tokens = cursor.fetchall()
|
||||||
|
|
||||||
|
# 5. 각 거래의 상품 상세 조회 (MSSQL)
|
||||||
|
purchases = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
session = db_manager.get_session('PM_PRES')
|
||||||
|
|
||||||
|
for token in claimed_tokens:
|
||||||
|
transaction_id = token['transaction_id']
|
||||||
|
|
||||||
|
# SALE_SUB + CD_GOODS JOIN
|
||||||
|
sale_items_query = text("""
|
||||||
|
SELECT
|
||||||
|
S.DrugCode,
|
||||||
|
ISNULL(G.GoodsName, '(약품명 없음)') AS goods_name,
|
||||||
|
S.SL_NM_item AS quantity,
|
||||||
|
S.SL_NM_cost_a AS price,
|
||||||
|
S.SL_TOTAL_PRICE AS total
|
||||||
|
FROM SALE_SUB S
|
||||||
|
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
|
||||||
|
WHERE S.SL_NO_order = :transaction_id
|
||||||
|
ORDER BY S.DrugCode
|
||||||
|
""")
|
||||||
|
|
||||||
|
items_raw = session.execute(
|
||||||
|
sale_items_query,
|
||||||
|
{'transaction_id': transaction_id}
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
# 상품 리스트 변환
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
'code': item.DrugCode,
|
||||||
|
'name': item.goods_name,
|
||||||
|
'qty': int(item.quantity or 0),
|
||||||
|
'price': int(item.price or 0),
|
||||||
|
'total': int(item.total or 0)
|
||||||
|
}
|
||||||
|
for item in items_raw
|
||||||
|
]
|
||||||
|
|
||||||
|
# 상품 요약 생성 ("첫번째상품명 외 N개")
|
||||||
|
if items:
|
||||||
|
first_item_name = items[0]['name']
|
||||||
|
items_count = len(items)
|
||||||
|
if items_count == 1:
|
||||||
|
items_summary = first_item_name
|
||||||
|
else:
|
||||||
|
items_summary = f"{first_item_name} 외 {items_count - 1}개"
|
||||||
|
else:
|
||||||
|
items_summary = "상품 정보 없음"
|
||||||
|
items_count = 0
|
||||||
|
|
||||||
|
purchases.append({
|
||||||
|
'transaction_id': transaction_id,
|
||||||
|
'date': str(token['claimed_at'])[:16].replace('T', ' '),
|
||||||
|
'amount': int(token['total_amount']),
|
||||||
|
'points': int(token['claimable_points']),
|
||||||
|
'items_summary': items_summary,
|
||||||
|
'items_count': items_count,
|
||||||
|
'items': items
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as mssql_error:
|
||||||
|
# MSSQL 연결 실패 시 빈 배열 반환
|
||||||
|
print(f"[WARNING] MSSQL 조회 실패 (user {user_id}): {mssql_error}")
|
||||||
|
purchases = []
|
||||||
|
|
||||||
|
# 6. 응답 생성
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'user': {
|
||||||
|
'id': user['id'],
|
||||||
|
'name': user['nickname'],
|
||||||
|
'phone': user['phone'],
|
||||||
|
'balance': user['mileage_balance'],
|
||||||
|
'created_at': str(user['created_at'])[:16].replace('T', ' ')
|
||||||
|
},
|
||||||
|
'mileage_history': [
|
||||||
|
{
|
||||||
|
'points': ml['points'],
|
||||||
|
'balance_after': ml['balance_after'],
|
||||||
|
'reason': ml['reason'],
|
||||||
|
'description': ml['description'],
|
||||||
|
'created_at': str(ml['created_at'])[:16].replace('T', ' '),
|
||||||
|
'transaction_id': ml['transaction_id']
|
||||||
|
}
|
||||||
|
for ml in mileage_history
|
||||||
|
],
|
||||||
|
'purchases': purchases
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'조회 실패: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin')
|
@app.route('/admin')
|
||||||
def admin():
|
def admin():
|
||||||
"""관리자 페이지 - 전체 사용자 및 적립 현황"""
|
"""관리자 페이지 - 전체 사용자 및 적립 현황"""
|
||||||
|
|||||||
@ -176,6 +176,31 @@
|
|||||||
padding: 10px 12px;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -230,9 +255,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for user in recent_users %}
|
{% for user in recent_users %}
|
||||||
<tr>
|
<tr style="cursor: pointer;" onclick="showUserDetail({{ user.id }})">
|
||||||
<td>{{ user.id }}</td>
|
<td>{{ user.id }}</td>
|
||||||
<td>{{ user.nickname }}</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="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 class="points-positive">{{ "{:,}".format(user.mileage_balance) }}P</td>
|
||||||
<td>{{ user.created_at[:16].replace('T', ' ') }}</td>
|
<td>{{ user.created_at[:16].replace('T', ' ') }}</td>
|
||||||
@ -342,6 +367,21 @@
|
|||||||
</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>
|
<script>
|
||||||
function showTransactionDetail(transactionId) {
|
function showTransactionDetail(transactionId) {
|
||||||
document.getElementById('transactionModal').style.display = 'block';
|
document.getElementById('transactionModal').style.display = 'block';
|
||||||
@ -469,6 +509,241 @@
|
|||||||
closeModal();
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user