feat: 어드민 적립내역 클릭 시 품목 상세 모달 + 키오스크 UI 개선
- 어드민 최근 적립 내역에서 행 클릭 시 MSSQL 품목 상세 모달 표시 - transaction_id가 있는 행만 클릭 가능 (돋보기 아이콘 표시) - 키오스크 품목 목록 표시, 세로 모니터 반응형 레이아웃 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
22cbf3d42e
commit
cb927d2207
@ -1791,7 +1791,8 @@ def admin():
|
||||
ml.balance_after,
|
||||
ml.reason,
|
||||
ml.description,
|
||||
ml.created_at
|
||||
ml.created_at,
|
||||
ml.transaction_id
|
||||
FROM mileage_ledger ml
|
||||
JOIN users u ON ml.user_id = u.id
|
||||
ORDER BY ml.created_at DESC
|
||||
@ -1912,12 +1913,35 @@ def api_kiosk_trigger():
|
||||
claimable_points = token_info['claimable_points']
|
||||
qr_url = token_info['qr_url']
|
||||
|
||||
# MSSQL에서 구매 품목 조회
|
||||
sale_items = []
|
||||
try:
|
||||
mssql_session = db_manager.get_session('PM_PRES')
|
||||
sale_sub_query = text("""
|
||||
SELECT
|
||||
ISNULL(G.GoodsName, '(약품명 없음)') AS goods_name,
|
||||
S.SL_NM_item AS quantity,
|
||||
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
|
||||
""")
|
||||
rows = mssql_session.execute(sale_sub_query, {'transaction_id': transaction_id}).fetchall()
|
||||
sale_items = [
|
||||
{'name': r.goods_name, 'qty': int(r.quantity or 0), 'total': int(r.total or 0)}
|
||||
for r in rows
|
||||
]
|
||||
except Exception as e:
|
||||
logging.warning(f"키오스크 품목 조회 실패 (transaction_id={transaction_id}): {e}")
|
||||
|
||||
# 키오스크 세션 저장
|
||||
kiosk_current_session = {
|
||||
'transaction_id': transaction_id,
|
||||
'amount': int(amount),
|
||||
'points': claimable_points,
|
||||
'qr_url': qr_url,
|
||||
'items': sale_items,
|
||||
'created_at': datetime.now(KST).isoformat()
|
||||
}
|
||||
|
||||
@ -1954,7 +1978,8 @@ def api_kiosk_current():
|
||||
'transaction_id': kiosk_current_session['transaction_id'],
|
||||
'amount': kiosk_current_session['amount'],
|
||||
'points': kiosk_current_session['points'],
|
||||
'qr_url': kiosk_current_session.get('qr_url')
|
||||
'qr_url': kiosk_current_session.get('qr_url'),
|
||||
'items': kiosk_current_session.get('items', [])
|
||||
})
|
||||
|
||||
|
||||
|
||||
@ -202,6 +202,11 @@
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.section table tbody tr[onclick]:hover {
|
||||
background: #eef2ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 사이드바 레이아웃 */
|
||||
.layout-wrapper {
|
||||
display: flex;
|
||||
@ -495,12 +500,12 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tx in recent_transactions %}
|
||||
<tr>
|
||||
<tr{% if tx.transaction_id %} onclick="showTransactionDetail('{{ tx.transaction_id }}')" style="cursor: pointer;" title="클릭하여 품목 상세 보기"{% endif %}>
|
||||
<td>{{ tx.nickname }}</td>
|
||||
<td class="phone-masked">{{ tx.phone[:3] }}-{{ tx.phone[3:7] }}-{{ tx.phone[7:] if tx.phone|length > 7 else '' }}</td>
|
||||
<td class="points-positive">{{ "{:,}".format(tx.points) }}P</td>
|
||||
<td>{{ "{:,}".format(tx.balance_after) }}P</td>
|
||||
<td>{{ tx.description or tx.reason }}</td>
|
||||
<td>{{ tx.description or tx.reason }}{% if tx.transaction_id %} <span style="color: #6366f1; font-size: 12px;">🔍</span>{% endif %}</td>
|
||||
<td>{{ tx.created_at[:16].replace('T', ' ') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@ -38,8 +38,9 @@
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.screen { display: none; width: 100%; height: 100%; }
|
||||
.screen { display: none; width: 100%; }
|
||||
.screen.active { display: flex; }
|
||||
|
||||
/* ══════════════════════════════════════
|
||||
@ -207,6 +208,39 @@
|
||||
.claim-amount-label { font-size: 15px; color: #6b7280; margin-bottom: 4px; }
|
||||
.claim-amount { font-size: 36px; font-weight: 900; color: #1e1b4b; letter-spacing: -1px; }
|
||||
.claim-points { font-size: 20px; color: #6366f1; font-weight: 700; margin-top: 8px; }
|
||||
/* 품목 카드 */
|
||||
.items-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.items-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #6b7280;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.items-list { display: flex; flex-direction: column; gap: 4px; }
|
||||
.item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
.item-row:last-child { border-bottom: none; }
|
||||
.item-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
|
||||
.item-qty { color: #9ca3af; font-size: 13px; flex-shrink: 0; }
|
||||
.item-total { font-weight: 600; color: #6366f1; flex-shrink: 0; min-width: 60px; text-align: right; }
|
||||
|
||||
.qr-container {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
@ -246,6 +280,7 @@
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: border-color 0.2s;
|
||||
min-height: 64px;
|
||||
}
|
||||
@ -256,7 +291,6 @@
|
||||
font-weight: 700;
|
||||
color: #9ca3af;
|
||||
letter-spacing: 1px;
|
||||
margin-right: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.phone-number {
|
||||
@ -264,8 +298,7 @@
|
||||
font-weight: 700;
|
||||
color: #1e1b4b;
|
||||
letter-spacing: 2px;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.phone-number.placeholder { color: #d1d5db; }
|
||||
|
||||
@ -348,9 +381,52 @@
|
||||
/* ── 적립/성공 화면 배경 밝게 ── */
|
||||
.claim-screen, .success-screen { background: #f5f7fa; border-radius: 24px; padding: 32px; }
|
||||
|
||||
/* ── 반응형 ── */
|
||||
/* ── 반응형: 세로 모니터 (portrait, 폭 700px 이상) ── */
|
||||
@media (orientation: portrait) and (min-width: 700px) {
|
||||
.main { padding: 32px; }
|
||||
|
||||
/* 적립 화면: 세로 스택, 공간 활용 */
|
||||
.claim-screen {
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 32px 48px;
|
||||
max-width: 640px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.claim-left {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.claim-info-card { flex: 1; min-width: 200px; }
|
||||
.qr-container { flex-shrink: 0; }
|
||||
.items-card { width: 100%; max-height: 160px; }
|
||||
.qr-container img { width: 140px; height: 140px; }
|
||||
|
||||
.divider { flex-direction: row; }
|
||||
.divider-line { width: 60px; height: 2px; }
|
||||
|
||||
.claim-right { width: 100%; align-items: center; }
|
||||
.phone-display-wrap { max-width: 440px; }
|
||||
.phone-prefix { font-size: 30px; }
|
||||
.phone-number { font-size: 30px; white-space: nowrap; }
|
||||
.numpad { max-width: 440px; }
|
||||
.submit-btn { max-width: 440px; }
|
||||
|
||||
/* 슬라이드 더 크게 */
|
||||
.slides-wrapper { height: 420px; }
|
||||
.slide-icon { width: 110px; height: 110px; font-size: 56px; }
|
||||
.slide-title { font-size: 34px; }
|
||||
.slide-desc { font-size: 19px; }
|
||||
.slide-highlight { font-size: 16px; padding: 12px 32px; }
|
||||
}
|
||||
|
||||
/* ── 반응형: 좁은 화면 (모바일) ── */
|
||||
@media (max-width: 700px) {
|
||||
.claim-screen { flex-direction: column; gap: 20px; padding: 20px; }
|
||||
.claim-screen { flex-direction: column; gap: 24px; padding: 20px; }
|
||||
.divider { flex-direction: row; }
|
||||
.divider-line { width: 60px; height: 2px; }
|
||||
.claim-amount { font-size: 28px; }
|
||||
@ -438,6 +514,10 @@
|
||||
<div class="claim-amount" id="claimAmount">0원</div>
|
||||
<div class="claim-points">적립 <span id="claimPoints">0</span>P</div>
|
||||
</div>
|
||||
<div class="items-card" id="itemsCard" style="display:none;">
|
||||
<div class="items-title">구매 품목</div>
|
||||
<div class="items-list" id="itemsList"></div>
|
||||
</div>
|
||||
<div class="qr-container" id="qrContainer" style="display:none;">
|
||||
<img id="qrImage" src="" alt="QR Code">
|
||||
<div class="qr-hint">휴대폰으로 QR을 스캔하여<br>적립할 수도 있습니다</div>
|
||||
@ -453,7 +533,7 @@
|
||||
<div class="claim-right">
|
||||
<div class="phone-section-title">전화번호로 적립하기</div>
|
||||
<div class="phone-display-wrap" id="phoneWrap">
|
||||
<span class="phone-prefix">010 -</span>
|
||||
<span class="phone-prefix">010-</span>
|
||||
<span class="phone-number placeholder" id="phoneDisplay">0000-0000</span>
|
||||
</div>
|
||||
<div class="error-msg" id="errorMsg"></div>
|
||||
@ -677,6 +757,22 @@ async function pollKioskSession() {
|
||||
document.getElementById('claimAmount').textContent = data.amount.toLocaleString() + '원';
|
||||
document.getElementById('claimPoints').textContent = data.points.toLocaleString();
|
||||
|
||||
// 품목 목록 표시
|
||||
const itemsCard = document.getElementById('itemsCard');
|
||||
const itemsList = document.getElementById('itemsList');
|
||||
if (data.items && data.items.length > 0) {
|
||||
itemsList.innerHTML = data.items.map(item =>
|
||||
`<div class="item-row">
|
||||
<span class="item-name">${item.name}</span>
|
||||
<span class="item-qty">${item.qty}개</span>
|
||||
<span class="item-total">${item.total.toLocaleString()}원</span>
|
||||
</div>`
|
||||
).join('');
|
||||
itemsCard.style.display = '';
|
||||
} else {
|
||||
itemsCard.style.display = 'none';
|
||||
}
|
||||
|
||||
if (data.qr_url) {
|
||||
document.getElementById('qrImage').src =
|
||||
'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=' +
|
||||
|
||||
Loading…
Reference in New Issue
Block a user