- /signup 회원가입 페이지 (이름 + 전화번호 + 개인정보 동의) - /api/signup API (get_or_create_user + 세션 저장) - 카카오 간편 가입 버튼 (카카오 로그인으로 가입) - 홈 화면에 회원가입 메뉴 추가 - 이미 로그인 시 /signup 접근하면 마이페이지로 리다이렉트 - 카카오 전화번호 수집 신청용 수집 사유 + 시나리오 문서 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
452 lines
15 KiB
HTML
452 lines
15 KiB
HTML
<!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="/signup" 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>
|
||
<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>
|