feat: 어드민 적립내역 클릭 시 품목 상세 모달 + 키오스크 UI 개선

- 어드민 최근 적립 내역에서 행 클릭 시 MSSQL 품목 상세 모달 표시
- transaction_id가 있는 행만 클릭 가능 (돋보기 아이콘 표시)
- 키오스크 품목 목록 표시, 세로 모니터 반응형 레이아웃 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
thug0bin 2026-02-25 16:37:50 +09:00
parent 22cbf3d42e
commit cb927d2207
3 changed files with 137 additions and 11 deletions

View File

@ -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', [])
})

View File

@ -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 %}

View File

@ -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=' +