feat: 홈 화면 리뉴얼 - QR 스캐너 + 카카오 로그인 + 세션 상태 표시

- 인라인 HTML → index.html 템플릿으로 전환
- HTML5 QR 코드 스캐너 (html5-qrcode 라이브러리)
- 로그인 상태에 따라 다른 메뉴 표시:
  - 비로그인: 카카오로 시작하기 + 마일리지 조회
  - 로그인: 내 마일리지 + 로그아웃
- QR 스캔 → /claim 자동 이동 (로그인 시 자동 적립)
- 디자인 시스템 통일 (Noto Sans KR, 보라색 그라디언트)
- PWA 메타 태그 + 개인정보 처리방침 푸터 링크

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
thug0bin 2026-02-25 08:57:55 +09:00
parent ed2a3f28bf
commit c4fa655005
2 changed files with 449 additions and 51 deletions

View File

@ -527,57 +527,12 @@ def find_user_by_kakao_id(kakao_id):
@app.route('/')
def index():
"""메인 페이지"""
return """
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>청춘약국 마일리지</title>
<style>
body {
font-family: 'Malgun Gothic', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 400px;
width: 100%;
text-align: center;
}
h1 {
color: #667eea;
margin-bottom: 30px;
font-size: 28px;
}
.info {
color: #666;
line-height: 1.8;
margin-bottom: 30px;
}
</style>
</head>
<body>
<div class="container">
<h1>🏥 청춘약국 마일리지</h1>
<div class="info">
영수증 QR 코드를 스캔하여<br>
마일리지를 적립해보세요!<br><br>
<strong>구매금액의 3%</strong><br>
포인트로 돌려드립니다.
</div>
</div>
</body>
</html>
"""
logged_in = 'logged_in_user_id' in session
return render_template('index.html',
logged_in=logged_in,
logged_in_name=session.get('logged_in_name', ''),
logged_in_phone=session.get('logged_in_phone', '')
)
@app.route('/claim')

View File

@ -0,0 +1,443 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#6366f1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="청춘약국">
<link rel="manifest" href="/static/manifest.json">
<link rel="apple-touch-icon" href="/static/icons/icon-192.png">
<link rel="icon" type="image/png" sizes="192x192" href="/static/icons/icon-192.png">
<title>청춘약국 마일리지</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f7fa;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.app-container {
background: #ffffff;
min-height: 100vh;
max-width: 420px;
margin: 0 auto;
}
.hero {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
padding: 48px 24px 56px;
text-align: center;
color: #ffffff;
position: relative;
}
.hero-logo {
width: 72px;
height: 72px;
background: rgba(255,255,255,0.15);
border-radius: 20px;
margin: 0 auto 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
}
.hero-title {
font-size: 26px;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 8px;
}
.hero-desc {
font-size: 15px;
font-weight: 400;
opacity: 0.85;
letter-spacing: -0.2px;
}
/* 로그인 상태 배지 */
.user-badge {
display: inline-flex;
align-items: center;
gap: 8px;
background: rgba(255,255,255,0.2);
padding: 8px 16px;
border-radius: 20px;
margin-top: 16px;
font-size: 14px;
font-weight: 500;
}
.main-content {
padding: 24px;
margin-top: -20px;
position: relative;
}
/* QR 스캔 버튼 */
.scan-card {
background: #ffffff;
border-radius: 20px;
padding: 28px 24px;
box-shadow: 0 8px 32px rgba(0,0,0,0.08);
text-align: center;
margin-bottom: 20px;
}
.scan-title {
font-size: 16px;
font-weight: 700;
color: #212529;
margin-bottom: 6px;
letter-spacing: -0.3px;
}
.scan-desc {
font-size: 13px;
color: #868e96;
margin-bottom: 20px;
letter-spacing: -0.2px;
}
.btn-scan {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 18px;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: #ffffff;
border: none;
border-radius: 16px;
font-size: 17px;
font-weight: 700;
cursor: pointer;
letter-spacing: -0.3px;
transition: all 0.2s ease;
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.3);
}
.btn-scan:active { transform: scale(0.98); }
.btn-scan svg {
width: 24px;
height: 24px;
fill: #ffffff;
}
/* 메뉴 카드들 */
.menu-grid {
display: flex;
flex-direction: column;
gap: 12px;
}
.menu-card {
display: flex;
align-items: center;
gap: 16px;
background: #ffffff;
border: 1px solid #e9ecef;
border-radius: 16px;
padding: 20px;
text-decoration: none;
transition: all 0.2s ease;
}
.menu-card:active {
transform: scale(0.98);
background: #f8f9fa;
}
.menu-icon {
width: 48px;
height: 48px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.menu-icon.purple { background: rgba(99, 102, 241, 0.1); }
.menu-icon.yellow { background: rgba(254, 229, 0, 0.3); }
.menu-icon.green { background: rgba(16, 185, 129, 0.1); }
.menu-text {
flex: 1;
}
.menu-title {
font-size: 15px;
font-weight: 700;
color: #212529;
letter-spacing: -0.3px;
margin-bottom: 2px;
}
.menu-desc {
font-size: 13px;
color: #868e96;
letter-spacing: -0.2px;
}
.menu-arrow {
color: #ced4da;
font-size: 18px;
}
/* QR 스캐너 모달 */
.scanner-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.95);
z-index: 1000;
flex-direction: column;
align-items: center;
justify-content: center;
}
.scanner-overlay.open { display: flex; }
.scanner-header {
position: absolute;
top: 0; left: 0; right: 0;
padding: 16px 24px;
padding-top: calc(16px + env(safe-area-inset-top, 0px));
display: flex;
justify-content: space-between;
align-items: center;
z-index: 10;
}
.scanner-title {
color: #ffffff;
font-size: 18px;
font-weight: 700;
}
.btn-close-scanner {
color: #ffffff;
background: rgba(255,255,255,0.15);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
#qr-reader {
width: 300px;
height: 300px;
border-radius: 20px;
overflow: hidden;
}
.scanner-hint {
color: rgba(255,255,255,0.7);
font-size: 14px;
margin-top: 24px;
text-align: center;
letter-spacing: -0.2px;
}
.footer {
text-align: center;
padding: 24px;
padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px));
}
.footer-link {
color: #adb5bd;
font-size: 12px;
text-decoration: none;
letter-spacing: -0.2px;
}
.footer-link + .footer-link { margin-left: 16px; }
@media (max-width: 480px) {
.hero {
padding-top: calc(48px + env(safe-area-inset-top, 0px));
}
}
</style>
</head>
<body>
<div class="app-container">
<div class="hero">
<div class="hero-logo">💊</div>
<div class="hero-title">청춘약국 마일리지</div>
<div class="hero-desc">구매금액의 3%를 포인트로 돌려드려요</div>
{% if logged_in %}
<div class="user-badge">
<span>{{ logged_in_name }}님 로그인 중</span>
</div>
{% endif %}
</div>
<div class="main-content">
<!-- QR 스캔 카드 -->
<div class="scan-card">
<div class="scan-title">영수증 QR 스캔</div>
<div class="scan-desc">
{% if logged_in %}
카메라로 QR을 찍으면 바로 적립됩니다
{% else %}
영수증의 QR 코드를 스캔하여 적립하세요
{% endif %}
</div>
<button class="btn-scan" id="btnOpenScanner">
<svg viewBox="0 0 24 24"><path d="M3 11h2V9H3v2zm0 2h2v-2H3v2zm4 8h2v-2H7v2zM3 15h2v-2H3v2zm8-12h-2v2h2V3zm4 0v2h2V3h-2zm-4 18h2v-2h-2v2zM3 3v2h2V3H3zm0 16h2v-2H3v2zm8-16h-2v2h2V3zm8 4h2V5h-2v2zm0 4h2V9h-2v2zm0-8v2h2V3h-2zm0 12h2v-2h-2v2zm0 4v-2h-2v2h2zm-4 0h2v-2h-2v2zm-8-8h10v-2H7v2zm0 4h6v-2H7v2zm4-8h2V7h-2v2zM3 7h2V5H3v2z"/></svg>
QR 코드 스캔하기
</button>
</div>
<!-- 메뉴 카드들 -->
<div class="menu-grid">
{% if logged_in %}
<a href="/my-page?phone={{ logged_in_phone }}" class="menu-card">
<div class="menu-icon purple">📊</div>
<div class="menu-text">
<div class="menu-title">내 마일리지</div>
<div class="menu-desc">적립 내역 및 포인트 확인</div>
</div>
<div class="menu-arrow"></div>
</a>
<a href="/logout" class="menu-card">
<div class="menu-icon green">🔓</div>
<div class="menu-text">
<div class="menu-title">로그아웃</div>
<div class="menu-desc">다른 계정으로 전환</div>
</div>
<div class="menu-arrow"></div>
</a>
{% else %}
<a href="/my-page/kakao/start" class="menu-card">
<div class="menu-icon yellow">
<svg width="22" height="22" viewBox="0 0 20 20" fill="none"><path d="M10 1C4.477 1 0 4.477 0 8.5c0 2.58 1.693 4.847 4.243 6.134l-1.084 3.97a.3.3 0 00.457.338L7.7 16.392c.75.112 1.52.17 2.3.17 5.523 0 10-3.477 10-7.562C20 4.477 15.523 1 10 1z" fill="#191919"/></svg>
</div>
<div class="menu-text">
<div class="menu-title">카카오로 시작하기</div>
<div class="menu-desc">로그인하면 QR만 찍으면 바로 적립</div>
</div>
<div class="menu-arrow"></div>
</a>
<a href="/my-page" class="menu-card">
<div class="menu-icon purple">📊</div>
<div class="menu-text">
<div class="menu-title">마일리지 조회</div>
<div class="menu-desc">전화번호로 적립 내역 확인</div>
</div>
<div class="menu-arrow"></div>
</a>
{% endif %}
</div>
</div>
<div class="footer">
<a href="/privacy" class="footer-link">개인정보 처리방침</a>
</div>
</div>
<!-- QR 스캐너 모달 -->
<div class="scanner-overlay" id="scannerOverlay">
<div class="scanner-header">
<div class="scanner-title">QR 코드 스캔</div>
<button class="btn-close-scanner" id="btnCloseScanner"></button>
</div>
<div id="qr-reader"></div>
<div class="scanner-hint">영수증의 QR 코드를 카메라에 비춰주세요</div>
</div>
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<script>
if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').catch(()=>{});}
(function() {
const overlay = document.getElementById('scannerOverlay');
const btnOpen = document.getElementById('btnOpenScanner');
const btnClose = document.getElementById('btnCloseScanner');
let scanner = null;
let scanning = false;
btnOpen.addEventListener('click', startScanner);
btnClose.addEventListener('click', stopScanner);
async function startScanner() {
overlay.classList.add('open');
if (scanner) {
scanner.clear();
}
scanner = new Html5Qrcode('qr-reader');
scanning = true;
try {
await scanner.start(
{ facingMode: 'environment' },
{ fps: 10, qrbox: { width: 250, height: 250 } },
onScanSuccess,
function() {} // ignore scan errors
);
} catch (err) {
document.querySelector('.scanner-hint').textContent =
'카메라 접근이 거부되었습니다. 설정에서 카메라 권한을 허용해주세요.';
}
}
async function stopScanner() {
if (scanner && scanning) {
try { await scanner.stop(); } catch(e) {}
scanning = false;
}
overlay.classList.remove('open');
}
function onScanSuccess(decodedText) {
// QR 코드 URL에서 /claim?t= 파라미터 추출
try {
let url;
if (decodedText.startsWith('http')) {
url = new URL(decodedText);
// 같은 도메인이거나 claim 경로면 이동
if (url.pathname.startsWith('/claim')) {
stopScanner();
window.location.href = url.pathname + url.search;
return;
}
}
// 직접 t= 값인 경우 (예: "12345:abc123")
if (decodedText.includes(':') && !decodedText.startsWith('http')) {
stopScanner();
window.location.href = '/claim?t=' + encodeURIComponent(decodedText);
return;
}
} catch(e) {}
// 인식은 됐지만 유효하지 않은 QR
document.querySelector('.scanner-hint').textContent =
'유효한 영수증 QR 코드가 아닙니다. 다시 시도해주세요.';
}
})();
</script>
</body>
</html>