Compare commits

...

4 Commits

Author SHA1 Message Date
622a143e19 fix: 거래 세부 내역 '수금' 필드를 '공급가액'으로 변경 및 부가세 표시 추가
- 부정확한 '수금' 레이블을 '공급가액'으로 수정
- 부가세 (SL_MY_rec_vat) 필드 추가 조회 및 표시
- 공급가액 + 부가세 = 판매 금액 구조로 명확화

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-23 19:13:31 +09:00
aa222eec3a fix: 거래 세부 내역의 단가 컬럼 매핑 수정
- SL_INPUT_PRICE → SL_NM_cost_a로 변경
- SL_INPUT_PRICE는 합계 금액이었음 (잘못된 매핑)
- SL_NM_cost_a가 실제 개당 단가

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-23 19:05:33 +09:00
10087cac5f fix: QR 라벨 개인정보 문구가 라벨 영역 내 표시되도록 간격 조정
- 포인트/안내 문구 간격 축소로 306px 라벨 내 수용
- y 최종 위치: 약 303px (라벨 높이 306px)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-23 18:55:43 +09:00
1717f4c6c2 feat: 개인정보 수집·이용 동의 프로세스 추가
- QR 라벨에 개인정보 동의 안내 문구 추가 (18pt 작은 글씨)
- 웹앱에 핀테크 스타일 개인정보 동의 체크박스 추가
- 백엔드 API에서 개인정보 동의 검증 추가
- 개인정보보호법 준수 강화

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-23 18:51:20 +09:00
5 changed files with 192 additions and 7 deletions

View File

@ -276,6 +276,7 @@ def api_claim():
nonce = data.get('nonce') nonce = data.get('nonce')
phone = data.get('phone', '').strip() phone = data.get('phone', '').strip()
name = data.get('name', '').strip() name = data.get('name', '').strip()
privacy_consent = data.get('privacy_consent', False)
# 입력 검증 # 입력 검증
if not phone or not name: if not phone or not name:
@ -284,6 +285,13 @@ def api_claim():
'message': '전화번호와 이름을 모두 입력해주세요.' 'message': '전화번호와 이름을 모두 입력해주세요.'
}), 400 }), 400
# 개인정보 동의 검증
if not privacy_consent:
return jsonify({
'success': False,
'message': '개인정보 수집·이용에 동의해주세요.'
}), 400
# 전화번호 형식 정리 (하이픈 제거) # 전화번호 형식 정리 (하이픈 제거)
phone = phone.replace('-', '').replace(' ', '') phone = phone.replace('-', '').replace(' ', '')
@ -383,6 +391,7 @@ def admin_transaction_detail(transaction_id):
SL_MY_sale, SL_MY_sale,
SL_MY_credit, SL_MY_credit,
SL_MY_recive, SL_MY_recive,
SL_MY_rec_vat,
ISNULL(SL_NM_custom, '[비고객]') AS customer_name ISNULL(SL_NM_custom, '[비고객]') AS customer_name
FROM SALE_MAIN FROM SALE_MAIN
WHERE SL_NO_order = :transaction_id WHERE SL_NO_order = :transaction_id
@ -402,7 +411,7 @@ def admin_transaction_detail(transaction_id):
S.DrugCode, S.DrugCode,
ISNULL(G.GoodsName, '(약품명 없음)') AS goods_name, ISNULL(G.GoodsName, '(약품명 없음)') AS goods_name,
S.SL_NM_item AS quantity, S.SL_NM_item AS quantity,
S.SL_INPUT_PRICE AS price, S.SL_NM_cost_a AS price,
S.SL_TOTAL_PRICE AS total S.SL_TOTAL_PRICE AS total
FROM SALE_SUB S FROM SALE_SUB S
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
@ -422,7 +431,8 @@ def admin_transaction_detail(transaction_id):
'discount': int(sale_main.SL_MY_discount or 0), 'discount': int(sale_main.SL_MY_discount or 0),
'sale_amount': int(sale_main.SL_MY_sale or 0), 'sale_amount': int(sale_main.SL_MY_sale or 0),
'credit': int(sale_main.SL_MY_credit or 0), 'credit': int(sale_main.SL_MY_credit or 0),
'received': int(sale_main.SL_MY_recive or 0), 'supply_value': int(sale_main.SL_MY_recive or 0),
'vat': int(sale_main.SL_MY_rec_vat or 0),
'customer_name': sale_main.customer_name 'customer_name': sale_main.customer_name
}, },
'items': [ 'items': [

View File

@ -0,0 +1,83 @@
"""
특정 거래의 SALE_SUB 데이터 확인
"""
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from db.dbsetup import DatabaseManager
from sqlalchemy import text
def check_sale_sub_data(transaction_id):
"""특정 거래의 판매 상세 데이터 확인"""
db_manager = DatabaseManager()
try:
session = db_manager.get_session('PM_PRES')
# SALE_SUB 모든 컬럼 조회
query = text("""
SELECT *
FROM SALE_SUB
WHERE SL_NO_order = :transaction_id
""")
result = session.execute(query, {'transaction_id': transaction_id}).fetchone()
if result:
print("=" * 80)
print(f"거래번호 {transaction_id}의 SALE_SUB 데이터")
print("=" * 80)
# 모든 컬럼 출력
for key in result._mapping.keys():
value = result._mapping[key]
print(f"{key:30} = {value}")
print("=" * 80)
else:
print(f"거래번호 {transaction_id}를 찾을 수 없습니다.")
except Exception as e:
print(f"오류 발생: {e}")
finally:
db_manager.close_all()
def check_sale_main_data(transaction_id):
"""특정 거래의 SALE_MAIN 데이터 확인"""
db_manager = DatabaseManager()
try:
session = db_manager.get_session('PM_PRES')
query = text("""
SELECT *
FROM SALE_MAIN
WHERE SL_NO_order = :transaction_id
""")
result = session.execute(query, {'transaction_id': transaction_id}).fetchone()
if result:
print("\n" + "=" * 80)
print(f"거래번호 {transaction_id}의 SALE_MAIN 데이터")
print("=" * 80)
for key in result._mapping.keys():
value = result._mapping[key]
print(f"{key:30} = {value}")
print("=" * 80)
else:
print(f"거래번호 {transaction_id}를 찾을 수 없습니다.")
except Exception as e:
print(f"오류 발생: {e}")
finally:
db_manager.close_all()
if __name__ == '__main__':
# 스크린샷의 거래번호
check_sale_sub_data('20260123000261')
check_sale_main_data('20260123000261')

View File

@ -407,8 +407,12 @@
<div style="color: #212529; font-size: 16px; font-weight: 600;">${tx.credit.toLocaleString()}원</div> <div style="color: #212529; font-size: 16px; font-weight: 600;">${tx.credit.toLocaleString()}원</div>
</div> </div>
<div> <div>
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">수금</div> <div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">공급가액</div>
<div style="color: #37b24d; font-size: 16px; font-weight: 600;">${tx.received.toLocaleString()}원</div> <div style="color: #37b24d; font-size: 16px; font-weight: 600;">${tx.supply_value.toLocaleString()}원</div>
</div>
<div>
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">부가세</div>
<div style="color: #495057; font-size: 16px; font-weight: 600;">${tx.vat.toLocaleString()}원</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -166,6 +166,72 @@
font-weight: 400; font-weight: 400;
} }
.privacy-consent {
margin: 24px 0 8px 0;
}
.checkbox-container {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
padding: 4px 0;
}
.checkbox-container input[type="checkbox"] {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
position: relative;
height: 24px;
width: 24px;
background-color: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 6px;
transition: all 0.2s ease;
flex-shrink: 0;
}
.checkbox-container:hover .checkmark {
border-color: #6366f1;
background-color: #ffffff;
}
.checkbox-container input:checked ~ .checkmark {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-color: #6366f1;
}
.checkmark::after {
content: "";
position: absolute;
display: none;
left: 7px;
top: 3px;
width: 6px;
height: 11px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-container input:checked ~ .checkmark::after {
display: block;
}
.consent-text {
margin-left: 12px;
color: #495057;
font-size: 14px;
font-weight: 500;
letter-spacing: -0.2px;
}
.btn-submit { .btn-submit {
width: 100%; width: 100%;
padding: 18px; padding: 18px;
@ -382,6 +448,14 @@
</div> </div>
</div> </div>
<div class="privacy-consent">
<label class="checkbox-container">
<input type="checkbox" id="privacyConsent" required>
<span class="checkmark"></span>
<span class="consent-text">개인정보 수집·이용 동의</span>
</label>
</div>
<button type="submit" class="btn-submit" id="btnSubmit"> <button type="submit" class="btn-submit" id="btnSubmit">
포인트 적립하기 포인트 적립하기
</button> </button>
@ -442,12 +516,18 @@
const phone = document.getElementById('phone').value.trim(); const phone = document.getElementById('phone').value.trim();
const name = document.getElementById('name').value.trim(); const name = document.getElementById('name').value.trim();
const privacyConsent = document.getElementById('privacyConsent').checked;
if (!phone || !name) { if (!phone || !name) {
showAlert('전화번호와 이름을 모두 입력해주세요.'); showAlert('전화번호와 이름을 모두 입력해주세요.');
return; return;
} }
if (!privacyConsent) {
showAlert('개인정보 수집·이용에 동의해주세요.');
return;
}
btnSubmit.disabled = true; btnSubmit.disabled = true;
btnSubmit.textContent = '처리 중...'; btnSubmit.textContent = '처리 중...';
alertMsg.style.display = 'none'; alertMsg.style.display = 'none';
@ -462,7 +542,8 @@
transaction_id: tokenInfo.transaction_id, transaction_id: tokenInfo.transaction_id,
nonce: tokenInfo.nonce, nonce: tokenInfo.nonce,
phone: phone, phone: phone,
name: name name: name,
privacy_consent: true
}) })
}); });

View File

@ -92,6 +92,7 @@ def create_qr_receipt_label(qr_url, transaction_id, total_amount, claimable_poin
font_amount = ImageFont.truetype(font_path, 40) # 금액 (크게) font_amount = ImageFont.truetype(font_path, 40) # 금액 (크게)
font_points = ImageFont.truetype(font_path, 36) # 포인트 (강조) font_points = ImageFont.truetype(font_path, 36) # 포인트 (강조)
font_small = ImageFont.truetype(font_path, 28) # 안내 문구 font_small = ImageFont.truetype(font_path, 28) # 안내 문구
font_tiny = ImageFont.truetype(font_path, 18) # 개인정보 동의 (작게)
else: else:
raise IOError("폰트 없음") raise IOError("폰트 없음")
except (IOError, OSError): except (IOError, OSError):
@ -101,6 +102,7 @@ def create_qr_receipt_label(qr_url, transaction_id, total_amount, claimable_poin
font_amount = ImageFont.load_default() font_amount = ImageFont.load_default()
font_points = ImageFont.load_default() font_points = ImageFont.load_default()
font_small = ImageFont.load_default() font_small = ImageFont.load_default()
font_tiny = ImageFont.load_default()
# 3. QR 코드 생성 (우측 상단) - 크기 및 해상도 개선 # 3. QR 코드 생성 (우측 상단) - 크기 및 해상도 개선
qr = qrcode.QRCode( qr = qrcode.QRCode(
@ -150,18 +152,23 @@ def create_qr_receipt_label(qr_url, transaction_id, total_amount, claimable_poin
for offset in [(0, 0), (1, 0), (0, 1), (1, 1)]: for offset in [(0, 0), (1, 0), (0, 1), (1, 1)]:
draw.text((x_margin + offset[0], y + offset[1]), amount_text, draw.text((x_margin + offset[0], y + offset[1]), amount_text,
font=font_amount, fill=0) font=font_amount, fill=0)
y += 50 y += 46 # 50 → 46으로 축소
# 적립 포인트 (굵게) # 적립 포인트 (굵게)
points_text = f"적립예정: {claimable_points:,}P" points_text = f"적립예정: {claimable_points:,}P"
for offset in [(0, 0), (1, 0), (0, 1), (1, 1)]: for offset in [(0, 0), (1, 0), (0, 1), (1, 1)]:
draw.text((x_margin + offset[0], y + offset[1]), points_text, draw.text((x_margin + offset[0], y + offset[1]), points_text,
font=font_points, fill=0) font=font_points, fill=0)
y += 55 y += 42 # 45 → 42로 축소
# 안내 문구 # 안내 문구
guide_text = "QR 촬영하고 포인트 받으세요!" guide_text = "QR 촬영하고 포인트 받으세요!"
draw.text((x_margin, y), guide_text, font=font_small, fill=0) draw.text((x_margin, y), guide_text, font=font_small, fill=0)
y += 26 # 30 → 26으로 축소
# 개인정보 동의 안내 (작은 글씨)
privacy_text = "(QR 스캔 시 개인정보 수집·이용에 동의한 것으로 간주됩니다)"
draw.text((x_margin, y), privacy_text, font=font_tiny, fill=0)
# 5. 테두리 (가위선 스타일) # 5. 테두리 (가위선 스타일)
for i in range(2): for i in range(2):