feat: GUI 칼럼 설정 저장, 010 전화번호 UX 개선, 품목 상세 조회
- GUI: SALES_COLUMNS 상수 정의, 칼럼 폭/윈도우 위치 gui_settings.json에 저장 - 전화번호 입력: 적립페이지/마이페이지에서 010 고정 + 뒷번호만 입력 - 적립페이지: MSSQL SALE_SUB에서 구매 품목 조회 및 토글 표시 - 마이페이지: 적립 내역 탭 시 품목 상세 AJAX 조회 (캐시 적용) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
774c199c1a
commit
82220a4a44
3
.gitignore
vendored
3
.gitignore
vendored
@ -86,3 +86,6 @@ docker-compose.override.yml
|
|||||||
tmp/
|
tmp/
|
||||||
*.tmp
|
*.tmp
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# GUI settings (user-specific)
|
||||||
|
gui_settings.json
|
||||||
|
|||||||
@ -536,8 +536,29 @@ def claim():
|
|||||||
if not success:
|
if not success:
|
||||||
return render_template('error.html', message=message)
|
return render_template('error.html', message=message)
|
||||||
|
|
||||||
# 간편 적립 페이지 렌더링
|
# MSSQL에서 구매 품목 조회
|
||||||
return render_template('claim_form.html', token_info=token_info)
|
sale_items = []
|
||||||
|
try:
|
||||||
|
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 = 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}")
|
||||||
|
|
||||||
|
return render_template('claim_form.html', token_info=token_info, sale_items=sale_items)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/claim', methods=['POST'])
|
@app.route('/api/claim', methods=['POST'])
|
||||||
@ -647,9 +668,9 @@ def my_page():
|
|||||||
user = dict(user_raw)
|
user = dict(user_raw)
|
||||||
user['created_at'] = utc_to_kst_str(user_raw['created_at'])
|
user['created_at'] = utc_to_kst_str(user_raw['created_at'])
|
||||||
|
|
||||||
# 적립 내역 조회
|
# 적립 내역 조회 (transaction_id 포함)
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT points, balance_after, reason, description, created_at
|
SELECT points, balance_after, reason, description, created_at, transaction_id
|
||||||
FROM mileage_ledger
|
FROM mileage_ledger
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
|
|||||||
@ -13,7 +13,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QDialog, QMessageBox, QDateEdit, QCheckBox
|
QDialog, QMessageBox, QDateEdit, QCheckBox
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QDate, QTimer
|
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QDate, QTimer
|
||||||
from PyQt5.QtGui import QFont
|
from PyQt5.QtGui import QFont, QColor
|
||||||
|
|
||||||
# 데이터베이스 연결 (backend/ 폴더를 Python 경로에 추가)
|
# 데이터베이스 연결 (backend/ 폴더를 Python 경로에 추가)
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
@ -556,6 +556,20 @@ class POSSalesGUI(QMainWindow):
|
|||||||
"""
|
"""
|
||||||
POS 판매 내역 조회 메인 GUI
|
POS 판매 내역 조회 메인 GUI
|
||||||
"""
|
"""
|
||||||
|
CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'gui_settings.json')
|
||||||
|
|
||||||
|
# 판매 테이블 컬럼 정의: (헤더명, 기본폭, 데이터키)
|
||||||
|
SALES_COLUMNS = [
|
||||||
|
('주문번호', 150, 'order_no'),
|
||||||
|
('시간', 70, 'time'),
|
||||||
|
('금액', 100, 'amount'),
|
||||||
|
('고객명', 80, 'customer'),
|
||||||
|
('품목수', 55, 'item_count'),
|
||||||
|
('적립자', 90, 'claimed_name'),
|
||||||
|
('전화번호', 120, 'claimed_phone'),
|
||||||
|
('적립포인트', 90, 'claimed_points'),
|
||||||
|
('QR', 50, 'qr_issued'),
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -563,12 +577,17 @@ class POSSalesGUI(QMainWindow):
|
|||||||
self.sales_thread = None
|
self.sales_thread = None
|
||||||
self.qr_thread = None # QR 생성 스레드 추가
|
self.qr_thread = None # QR 생성 스레드 추가
|
||||||
self.sales_data = []
|
self.sales_data = []
|
||||||
|
self._gui_settings = self._load_settings()
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
"""UI 초기화"""
|
"""UI 초기화"""
|
||||||
self.setWindowTitle('POS 판매 조회')
|
self.setWindowTitle('POS 판매 조회')
|
||||||
self.setGeometry(100, 100, 1300, 600)
|
saved_geo = self._gui_settings.get('window_geometry')
|
||||||
|
if saved_geo and len(saved_geo) == 4:
|
||||||
|
self.setGeometry(*saved_geo)
|
||||||
|
else:
|
||||||
|
self.setGeometry(100, 100, 1300, 600)
|
||||||
|
|
||||||
# 중앙 위젯
|
# 중앙 위젯
|
||||||
central_widget = QWidget()
|
central_widget = QWidget()
|
||||||
@ -659,19 +678,18 @@ class POSSalesGUI(QMainWindow):
|
|||||||
sales_group.setLayout(sales_layout)
|
sales_group.setLayout(sales_layout)
|
||||||
|
|
||||||
self.sales_table = QTableWidget()
|
self.sales_table = QTableWidget()
|
||||||
self.sales_table.setColumnCount(9)
|
col_count = len(self.SALES_COLUMNS)
|
||||||
self.sales_table.setHorizontalHeaderLabels([
|
self.sales_table.setColumnCount(col_count)
|
||||||
'주문번호', '시간', '금액', '고객명', '품목수', '적립자명', '전화번호', '적립포인트', 'QR'
|
self.sales_table.setHorizontalHeaderLabels([c[0] for c in self.SALES_COLUMNS])
|
||||||
])
|
|
||||||
self.sales_table.setColumnWidth(0, 160)
|
# 컬럼 폭: 저장된 값 우선, 없으면 SALES_COLUMNS 기본값
|
||||||
self.sales_table.setColumnWidth(1, 70)
|
saved_widths = self._gui_settings.get('sales_column_widths')
|
||||||
self.sales_table.setColumnWidth(2, 110)
|
for i, (_, default_w, _) in enumerate(self.SALES_COLUMNS):
|
||||||
self.sales_table.setColumnWidth(3, 100)
|
w = saved_widths[i] if saved_widths and len(saved_widths) == col_count else default_w
|
||||||
self.sales_table.setColumnWidth(4, 70)
|
self.sales_table.setColumnWidth(i, w)
|
||||||
self.sales_table.setColumnWidth(5, 100)
|
|
||||||
self.sales_table.setColumnWidth(6, 120)
|
self.sales_table.horizontalHeader().setStretchLastSection(True)
|
||||||
self.sales_table.setColumnWidth(7, 100)
|
self.sales_table.horizontalHeader().sectionResized.connect(self._on_column_resized)
|
||||||
self.sales_table.setColumnWidth(8, 60)
|
|
||||||
self.sales_table.setSelectionBehavior(QTableWidget.SelectRows)
|
self.sales_table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||||
self.sales_table.doubleClicked.connect(self.show_sale_detail)
|
self.sales_table.doubleClicked.connect(self.show_sale_detail)
|
||||||
self.sales_table.cellClicked.connect(self.on_cell_clicked)
|
self.sales_table.cellClicked.connect(self.on_cell_clicked)
|
||||||
@ -745,79 +763,85 @@ class POSSalesGUI(QMainWindow):
|
|||||||
|
|
||||||
def populate_table(self, sales_list):
|
def populate_table(self, sales_list):
|
||||||
"""QTableWidget에 데이터 채우기"""
|
"""QTableWidget에 데이터 채우기"""
|
||||||
|
# 컬럼 인덱스 맵 (SALES_COLUMNS 순서 기반)
|
||||||
|
COL = {key: i for i, (_, _, key) in enumerate(self.SALES_COLUMNS)}
|
||||||
|
|
||||||
self.sales_table.setRowCount(len(sales_list))
|
self.sales_table.setRowCount(len(sales_list))
|
||||||
|
|
||||||
|
# 적립 완료 셀 스타일
|
||||||
|
CLAIMED_COLOR = QColor('#4CAF50')
|
||||||
|
def make_claimed_font(underline=True):
|
||||||
|
f = QFont()
|
||||||
|
f.setBold(True)
|
||||||
|
if underline:
|
||||||
|
f.setUnderline(True)
|
||||||
|
return f
|
||||||
|
|
||||||
for row, sale in enumerate(sales_list):
|
for row, sale in enumerate(sales_list):
|
||||||
# 주문번호
|
# 주문번호
|
||||||
self.sales_table.setItem(row, 0, QTableWidgetItem(sale['order_no']))
|
self.sales_table.setItem(row, COL['order_no'],
|
||||||
|
QTableWidgetItem(sale['order_no']))
|
||||||
|
|
||||||
# 시간
|
# 시간
|
||||||
self.sales_table.setItem(row, 1, QTableWidgetItem(sale['time']))
|
self.sales_table.setItem(row, COL['time'],
|
||||||
|
QTableWidgetItem(sale['time']))
|
||||||
|
|
||||||
# 금액 (우측 정렬, 천단위 콤마)
|
# 금액 (우측 정렬, 천단위 콤마)
|
||||||
amount_item = QTableWidgetItem(f"{sale['amount']:,.0f}원")
|
amount_item = QTableWidgetItem(f"{sale['amount']:,.0f}원")
|
||||||
amount_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
amount_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||||
self.sales_table.setItem(row, 2, amount_item)
|
self.sales_table.setItem(row, COL['amount'], amount_item)
|
||||||
|
|
||||||
# 고객명 (MSSQL)
|
# 고객명 (MSSQL POS)
|
||||||
self.sales_table.setItem(row, 3, QTableWidgetItem(sale['customer']))
|
self.sales_table.setItem(row, COL['customer'],
|
||||||
|
QTableWidgetItem(sale['customer']))
|
||||||
|
|
||||||
# 품목수 (중앙 정렬)
|
# 품목수 (중앙 정렬)
|
||||||
count_item = QTableWidgetItem(str(sale['item_count']))
|
count_item = QTableWidgetItem(str(sale['item_count']))
|
||||||
count_item.setTextAlignment(Qt.AlignCenter)
|
count_item.setTextAlignment(Qt.AlignCenter)
|
||||||
self.sales_table.setItem(row, 4, count_item)
|
self.sales_table.setItem(row, COL['item_count'], count_item)
|
||||||
|
|
||||||
# 적립자명 (SQLite)
|
# 적립자 (SQLite 마일리지)
|
||||||
from PyQt5.QtGui import QColor, QFont
|
|
||||||
claimed_name_item = QTableWidgetItem(sale['claimed_name'])
|
claimed_name_item = QTableWidgetItem(sale['claimed_name'])
|
||||||
if sale['claimed_name']:
|
if sale['claimed_name']:
|
||||||
claimed_name_item.setForeground(QColor('#4CAF50'))
|
claimed_name_item.setForeground(CLAIMED_COLOR)
|
||||||
font = QFont()
|
claimed_name_item.setFont(make_claimed_font())
|
||||||
font.setBold(True)
|
|
||||||
font.setUnderline(True) # 밑줄 추가로 클릭 가능 표시
|
|
||||||
claimed_name_item.setFont(font)
|
|
||||||
claimed_name_item.setToolTip('클릭하여 회원 마일리지 내역 보기')
|
claimed_name_item.setToolTip('클릭하여 회원 마일리지 내역 보기')
|
||||||
self.sales_table.setItem(row, 5, claimed_name_item)
|
self.sales_table.setItem(row, COL['claimed_name'], claimed_name_item)
|
||||||
|
|
||||||
# 전화번호 (SQLite)
|
# 전화번호 (SQLite 마일리지)
|
||||||
claimed_phone_item = QTableWidgetItem(sale['claimed_phone'])
|
claimed_phone_item = QTableWidgetItem(sale['claimed_phone'])
|
||||||
if sale['claimed_phone']:
|
if sale['claimed_phone']:
|
||||||
claimed_phone_item.setForeground(QColor('#4CAF50'))
|
claimed_phone_item.setForeground(CLAIMED_COLOR)
|
||||||
font = QFont()
|
claimed_phone_item.setFont(make_claimed_font())
|
||||||
font.setBold(True)
|
|
||||||
font.setUnderline(True) # 밑줄 추가로 클릭 가능 표시
|
|
||||||
claimed_phone_item.setFont(font)
|
|
||||||
claimed_phone_item.setToolTip('클릭하여 회원 마일리지 내역 보기')
|
claimed_phone_item.setToolTip('클릭하여 회원 마일리지 내역 보기')
|
||||||
self.sales_table.setItem(row, 6, claimed_phone_item)
|
self.sales_table.setItem(row, COL['claimed_phone'], claimed_phone_item)
|
||||||
|
|
||||||
# 적립포인트 (SQLite)
|
# 적립포인트 (SQLite 마일리지)
|
||||||
claimed_points_item = QTableWidgetItem(f"{sale['claimed_points']:,}P" if sale['claimed_points'] > 0 else "")
|
points_text = f"{sale['claimed_points']:,}P" if sale['claimed_points'] > 0 else ""
|
||||||
|
claimed_points_item = QTableWidgetItem(points_text)
|
||||||
if sale['claimed_points'] > 0:
|
if sale['claimed_points'] > 0:
|
||||||
claimed_points_item.setForeground(QColor('#4CAF50'))
|
claimed_points_item.setForeground(CLAIMED_COLOR)
|
||||||
claimed_points_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
claimed_points_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||||
font = QFont()
|
claimed_points_item.setFont(make_claimed_font())
|
||||||
font.setBold(True)
|
|
||||||
font.setUnderline(True) # 밑줄 추가로 클릭 가능 표시
|
|
||||||
claimed_points_item.setFont(font)
|
|
||||||
claimed_points_item.setToolTip('클릭하여 회원 마일리지 내역 보기')
|
claimed_points_item.setToolTip('클릭하여 회원 마일리지 내역 보기')
|
||||||
self.sales_table.setItem(row, 7, claimed_points_item)
|
self.sales_table.setItem(row, COL['claimed_points'], claimed_points_item)
|
||||||
|
|
||||||
# QR 발행 여부 (SQLite)
|
# QR 발행 여부
|
||||||
qr_status_item = QTableWidgetItem()
|
qr_status_item = QTableWidgetItem()
|
||||||
qr_status_item.setTextAlignment(Qt.AlignCenter)
|
qr_status_item.setTextAlignment(Qt.AlignCenter)
|
||||||
if sale['qr_issued']:
|
if sale['qr_issued']:
|
||||||
qr_status_item.setText('✓')
|
qr_status_item.setText('✓')
|
||||||
qr_status_item.setForeground(QColor('#4CAF50'))
|
qr_status_item.setForeground(CLAIMED_COLOR)
|
||||||
font = QFont()
|
f = QFont()
|
||||||
font.setBold(True)
|
f.setBold(True)
|
||||||
font.setPointSize(14)
|
f.setPointSize(14)
|
||||||
qr_status_item.setFont(font)
|
qr_status_item.setFont(f)
|
||||||
qr_status_item.setToolTip('QR 발행 완료')
|
qr_status_item.setToolTip('QR 발행 완료')
|
||||||
else:
|
else:
|
||||||
qr_status_item.setText('-')
|
qr_status_item.setText('-')
|
||||||
qr_status_item.setForeground(QColor('#BDBDBD'))
|
qr_status_item.setForeground(QColor('#BDBDBD'))
|
||||||
qr_status_item.setToolTip('QR 미발행')
|
qr_status_item.setToolTip('QR 미발행')
|
||||||
self.sales_table.setItem(row, 8, qr_status_item)
|
self.sales_table.setItem(row, COL['qr_issued'], qr_status_item)
|
||||||
|
|
||||||
def on_query_error(self, error_msg):
|
def on_query_error(self, error_msg):
|
||||||
"""DB 조회 에러 처리"""
|
"""DB 조회 에러 처리"""
|
||||||
@ -1003,8 +1027,36 @@ class POSSalesGUI(QMainWindow):
|
|||||||
self.status_label.setStyleSheet('color: red; font-size: 12px; padding: 5px;')
|
self.status_label.setStyleSheet('color: red; font-size: 12px; padding: 5px;')
|
||||||
QMessageBox.critical(self, '오류', f'QR 생성 실패:\n{message}')
|
QMessageBox.critical(self, '오류', f'QR 생성 실패:\n{message}')
|
||||||
|
|
||||||
|
# --- 설정 저장/로드 ---
|
||||||
|
def _load_settings(self):
|
||||||
|
try:
|
||||||
|
with open(self.CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_settings(self):
|
||||||
|
try:
|
||||||
|
with open(self.CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self._gui_settings, f, ensure_ascii=False, indent=2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_column_resized(self, index, old_size, new_size):
|
||||||
|
widths = [self.sales_table.columnWidth(i) for i in range(self.sales_table.columnCount())]
|
||||||
|
self._gui_settings['sales_column_widths'] = widths
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""종료 시 정리"""
|
"""종료 시 정리 + 설정 저장"""
|
||||||
|
# 컬럼 폭 최종 저장
|
||||||
|
if hasattr(self, 'sales_table'):
|
||||||
|
widths = [self.sales_table.columnWidth(i) for i in range(self.sales_table.columnCount())]
|
||||||
|
self._gui_settings['sales_column_widths'] = widths
|
||||||
|
# 윈도우 위치/크기 저장
|
||||||
|
geo = self.geometry()
|
||||||
|
self._gui_settings['window_geometry'] = [geo.x(), geo.y(), geo.width(), geo.height()]
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
# 자동 새로고침 타이머 중지
|
# 자동 새로고침 타이머 중지
|
||||||
if hasattr(self, 'refresh_timer'):
|
if hasattr(self, 'refresh_timer'):
|
||||||
self.refresh_timer.stop()
|
self.refresh_timer.stop()
|
||||||
|
|||||||
@ -121,6 +121,81 @@
|
|||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 구매 품목 리스트 */
|
||||||
|
.items-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
width: 100%;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-toggle .arrow {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-toggle.open .arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-list {
|
||||||
|
display: none;
|
||||||
|
border-top: 1px solid #f1f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-list.open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #f8f9fa;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
color: #495057;
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.2px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-qty {
|
||||||
|
color: #868e96;
|
||||||
|
margin-right: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-price {
|
||||||
|
color: #212529;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.form-section {
|
.form-section {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
@ -426,15 +501,36 @@
|
|||||||
<div class="points-badge">{{ "{:,}".format(token_info.claimable_points) }}P 적립</div>
|
<div class="points-badge">{{ "{:,}".format(token_info.claimable_points) }}P 적립</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if sale_items %}
|
||||||
|
<div class="items-section">
|
||||||
|
<button type="button" class="items-toggle" id="itemsToggle" onclick="toggleItems()">
|
||||||
|
<span>구매 품목 ({{ sale_items|length }}건)</span>
|
||||||
|
<span class="arrow">▼</span>
|
||||||
|
</button>
|
||||||
|
<div class="items-list" id="itemsList">
|
||||||
|
{% for item in sale_items %}
|
||||||
|
<div class="item-row">
|
||||||
|
<span class="item-name">{{ item.name }}</span>
|
||||||
|
<span class="item-qty">{{ item.qty }}개</span>
|
||||||
|
<span class="item-price">{{ "{:,}".format(item.total) }}원</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form id="formClaim" class="form-section">
|
<form id="formClaim" class="form-section">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="phone">전화번호</label>
|
<label for="phone">전화번호</label>
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper" style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span style="font-size: 18px; font-weight: 600; color: #495057; white-space: nowrap; padding: 16px 0 16px 4px;">010 -</span>
|
||||||
<input type="tel" id="phone" name="phone"
|
<input type="tel" id="phone" name="phone"
|
||||||
placeholder="010-0000-0000"
|
placeholder="0000-0000"
|
||||||
pattern="[0-9-]*"
|
inputmode="numeric"
|
||||||
|
maxlength="9"
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
required>
|
required
|
||||||
|
style="flex: 1;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -496,25 +592,36 @@
|
|||||||
const claimFormDiv = document.getElementById('claimForm');
|
const claimFormDiv = document.getElementById('claimForm');
|
||||||
const successScreen = document.getElementById('successScreen');
|
const successScreen = document.getElementById('successScreen');
|
||||||
|
|
||||||
// 전화번호 자동 하이픈
|
// 품목 토글
|
||||||
|
function toggleItems() {
|
||||||
|
const btn = document.getElementById('itemsToggle');
|
||||||
|
const list = document.getElementById('itemsList');
|
||||||
|
if (btn && list) {
|
||||||
|
btn.classList.toggle('open');
|
||||||
|
list.classList.toggle('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 뒷번호 자동 하이픈 (010 고정)
|
||||||
const phoneInput = document.getElementById('phone');
|
const phoneInput = document.getElementById('phone');
|
||||||
phoneInput.addEventListener('input', function(e) {
|
phoneInput.addEventListener('input', function(e) {
|
||||||
let value = e.target.value.replace(/[^0-9]/g, '');
|
let value = e.target.value.replace(/[^0-9]/g, '');
|
||||||
|
if (value.length <= 4) {
|
||||||
if (value.length <= 3) {
|
|
||||||
e.target.value = value;
|
e.target.value = value;
|
||||||
} else if (value.length <= 7) {
|
|
||||||
e.target.value = value.slice(0, 3) + '-' + value.slice(3);
|
|
||||||
} else {
|
} else {
|
||||||
e.target.value = value.slice(0, 3) + '-' + value.slice(3, 7) + '-' + value.slice(7, 11);
|
e.target.value = value.slice(0, 4) + '-' + value.slice(4, 8);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 포커스 시 자동으로 전화번호 필드로
|
||||||
|
phoneInput.focus();
|
||||||
|
|
||||||
// 폼 제출
|
// 폼 제출
|
||||||
form.addEventListener('submit', async function(e) {
|
form.addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const phone = document.getElementById('phone').value.trim();
|
const phoneRaw = document.getElementById('phone').value.trim().replace(/[^0-9]/g, '');
|
||||||
|
const phone = '010-' + phoneRaw.slice(0, 4) + '-' + phoneRaw.slice(4, 8);
|
||||||
const name = document.getElementById('name').value.trim();
|
const name = document.getElementById('name').value.trim();
|
||||||
const privacyConsent = document.getElementById('privacyConsent').checked;
|
const privacyConsent = document.getElementById('privacyConsent').checked;
|
||||||
|
|
||||||
|
|||||||
@ -194,6 +194,64 @@
|
|||||||
letter-spacing: -0.2px;
|
letter-spacing: -0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 품목 상세 */
|
||||||
|
.transaction-item.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-detail {
|
||||||
|
display: none;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #f1f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-detail.open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-detail-name {
|
||||||
|
color: #495057;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-detail-qty {
|
||||||
|
color: #868e96;
|
||||||
|
margin-right: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-detail-price {
|
||||||
|
color: #212529;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-detail-loading {
|
||||||
|
text-align: center;
|
||||||
|
color: #adb5bd;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-detail-hint {
|
||||||
|
color: #adb5bd;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 모바일 최적화 */
|
/* 모바일 최적화 */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.header {
|
.header {
|
||||||
@ -228,7 +286,8 @@
|
|||||||
{% if transactions %}
|
{% if transactions %}
|
||||||
<ul class="transaction-list">
|
<ul class="transaction-list">
|
||||||
{% for tx in transactions %}
|
{% for tx in transactions %}
|
||||||
<li class="transaction-item">
|
<li class="transaction-item {% if tx.transaction_id %}clickable{% endif %}"
|
||||||
|
{% if tx.transaction_id %}onclick="toggleDetail(this, '{{ tx.transaction_id }}')"{% endif %}>
|
||||||
<div class="transaction-header">
|
<div class="transaction-header">
|
||||||
<div class="transaction-reason">
|
<div class="transaction-reason">
|
||||||
{% if tx.reason == 'CLAIM' %}
|
{% if tx.reason == 'CLAIM' %}
|
||||||
@ -246,7 +305,13 @@
|
|||||||
{% if tx.description %}
|
{% if tx.description %}
|
||||||
<div class="transaction-desc">{{ tx.description }}</div>
|
<div class="transaction-desc">{{ tx.description }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="transaction-date">{{ tx.created_at }}</div>
|
<div class="transaction-date">
|
||||||
|
{{ tx.created_at }}
|
||||||
|
{% if tx.transaction_id %}
|
||||||
|
<span class="item-detail-hint">탭하여 품목 보기</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="item-detail" id="detail-{{ tx.transaction_id }}"></div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -258,5 +323,53 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const detailCache = {};
|
||||||
|
|
||||||
|
async function toggleDetail(el, txId) {
|
||||||
|
const detail = document.getElementById('detail-' + txId);
|
||||||
|
if (!detail) return;
|
||||||
|
|
||||||
|
// 이미 열려있으면 닫기
|
||||||
|
if (detail.classList.contains('open')) {
|
||||||
|
detail.classList.remove('open');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐시에 있으면 바로 표시
|
||||||
|
if (detailCache[txId]) {
|
||||||
|
detail.innerHTML = detailCache[txId];
|
||||||
|
detail.classList.add('open');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로딩 표시
|
||||||
|
detail.innerHTML = '<div class="item-detail-loading">품목 조회 중...</div>';
|
||||||
|
detail.classList.add('open');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/admin/transaction/' + txId);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success && data.items && data.items.length > 0) {
|
||||||
|
let html = '';
|
||||||
|
data.items.forEach(item => {
|
||||||
|
html += `<div class="item-detail-row">
|
||||||
|
<span class="item-detail-name">${item.name}</span>
|
||||||
|
<span class="item-detail-qty">${item.qty}개</span>
|
||||||
|
<span class="item-detail-price">${item.total.toLocaleString()}원</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
detailCache[txId] = html;
|
||||||
|
detail.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
detail.innerHTML = '<div class="item-detail-loading">품목 정보를 불러올 수 없습니다</div>';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
detail.innerHTML = '<div class="item-detail-loading">조회 실패</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -149,11 +149,17 @@
|
|||||||
<form method="GET" action="/my-page">
|
<form method="GET" action="/my-page">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="phone">전화번호</label>
|
<label for="phone">전화번호</label>
|
||||||
<input type="tel" id="phone" name="phone"
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
placeholder="010-0000-0000"
|
<span style="font-size: 18px; font-weight: 600; color: #495057; white-space: nowrap; padding: 16px 0 16px 4px;">010 -</span>
|
||||||
pattern="[0-9-]*"
|
<input type="tel" id="phoneInput"
|
||||||
autocomplete="tel"
|
placeholder="0000-0000"
|
||||||
required>
|
inputmode="numeric"
|
||||||
|
maxlength="9"
|
||||||
|
autocomplete="tel"
|
||||||
|
required
|
||||||
|
style="flex: 1;">
|
||||||
|
<input type="hidden" id="phone" name="phone">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn-submit">
|
<button type="submit" class="btn-submit">
|
||||||
@ -165,19 +171,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 전화번호 자동 하이픈
|
// 뒷번호 자동 하이픈 (010 고정)
|
||||||
const phoneInput = document.getElementById('phone');
|
const phoneInput = document.getElementById('phoneInput');
|
||||||
|
const phoneHidden = document.getElementById('phone');
|
||||||
|
|
||||||
phoneInput.addEventListener('input', function(e) {
|
phoneInput.addEventListener('input', function(e) {
|
||||||
let value = e.target.value.replace(/[^0-9]/g, '');
|
let value = e.target.value.replace(/[^0-9]/g, '');
|
||||||
|
if (value.length <= 4) {
|
||||||
if (value.length <= 3) {
|
|
||||||
e.target.value = value;
|
e.target.value = value;
|
||||||
} else if (value.length <= 7) {
|
|
||||||
e.target.value = value.slice(0, 3) + '-' + value.slice(3);
|
|
||||||
} else {
|
} else {
|
||||||
e.target.value = value.slice(0, 3) + '-' + value.slice(3, 7) + '-' + value.slice(7, 11);
|
e.target.value = value.slice(0, 4) + '-' + value.slice(4, 8);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 제출 시 010 합쳐서 hidden 필드에 전달
|
||||||
|
phoneInput.closest('form').addEventListener('submit', function() {
|
||||||
|
const raw = phoneInput.value.replace(/[^0-9]/g, '');
|
||||||
|
phoneHidden.value = '010' + raw;
|
||||||
|
});
|
||||||
|
|
||||||
|
phoneInput.focus();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user