178 lines
6.3 KiB
TypeScript
178 lines
6.3 KiB
TypeScript
/*
|
|
* QR 풀스크린 뷰어(SCR-M15 3) — 대형 QR + 이름·유형 + 밝기 부스트 안내.
|
|
* 하단 스와이프 인디케이터로 다른 티켓 전환(좌우 버튼). 예매번호 마스킹 노출.
|
|
* ※ 자동 밝기 상승은 expo-brightness 미설치 → 안내 문구 + 갭 기록(placeholder).
|
|
*/
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import React from 'react';
|
|
import { Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
import { colors, radius, spacing, touch, type } from '../../theme';
|
|
import { QrPlaceholder } from './QrPlaceholder';
|
|
import type { SampleTicket } from './sampleTickets';
|
|
|
|
export function QrViewerModal({
|
|
visible,
|
|
tickets,
|
|
index,
|
|
onChangeIndex,
|
|
onClose,
|
|
}: {
|
|
visible: boolean;
|
|
tickets: SampleTicket[];
|
|
index: number;
|
|
onChangeIndex: (i: number) => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const ticket = tickets[index];
|
|
if (!ticket) return null;
|
|
|
|
const canPrev = index > 0;
|
|
const canNext = index < tickets.length - 1;
|
|
|
|
return (
|
|
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
|
|
<View style={styles.backdrop}>
|
|
<View style={styles.sheet}>
|
|
{/* 헤더 (그라디언트 대체: 브랜드 딥블루 단색) */}
|
|
<View style={styles.header}>
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
accessibilityLabel="닫기"
|
|
style={styles.closeBtn}
|
|
onPress={onClose}
|
|
>
|
|
<Ionicons name="close" size={24} color={colors.white} />
|
|
</Pressable>
|
|
<Text style={styles.headerTitle}>입장 QR 코드</Text>
|
|
<Text style={styles.headerSub} numberOfLines={1}>
|
|
{ticket.eventName}
|
|
</Text>
|
|
</View>
|
|
|
|
<ScrollView contentContainerStyle={styles.content}>
|
|
<QrPlaceholder seed={ticket.qrSeed} size={200} />
|
|
|
|
<View style={styles.nameRow}>
|
|
<Text style={styles.holderName}>{ticket.holderName}</Text>
|
|
<Text style={styles.holderType}> {ticket.ticketType}</Text>
|
|
</View>
|
|
<View style={styles.bookingChip}>
|
|
<Text style={styles.bookingText}>{ticket.bookingNoMasked}</Text>
|
|
</View>
|
|
|
|
{/* 밝기 부스트 안내 */}
|
|
<View style={styles.hint}>
|
|
<Ionicons name="bulb-outline" size={20} color={colors.primary600} />
|
|
<Text style={styles.hintText}>
|
|
입장 시 화면을 <Text style={styles.hintBold}>최대 밝기</Text>로 유지하세요
|
|
</Text>
|
|
</View>
|
|
|
|
{/* 스와이프(버튼) 전환 */}
|
|
{tickets.length > 1 ? (
|
|
<View style={styles.swipeRow}>
|
|
<Pressable
|
|
accessibilityLabel="이전 티켓"
|
|
disabled={!canPrev}
|
|
onPress={() => onChangeIndex(index - 1)}
|
|
style={[styles.navBtn, !canPrev && styles.navBtnOff]}
|
|
>
|
|
<Ionicons name="chevron-back" size={22} color={canPrev ? colors.primary600 : colors.neutral200} />
|
|
</Pressable>
|
|
<View style={styles.dots}>
|
|
{tickets.map((t, i) => (
|
|
<View
|
|
key={t.id}
|
|
style={[styles.dot, i === index ? styles.dotOn : null]}
|
|
/>
|
|
))}
|
|
</View>
|
|
<Pressable
|
|
accessibilityLabel="다음 티켓"
|
|
disabled={!canNext}
|
|
onPress={() => onChangeIndex(index + 1)}
|
|
style={[styles.navBtn, !canNext && styles.navBtnOff]}
|
|
>
|
|
<Ionicons name="chevron-forward" size={22} color={canNext ? colors.primary600 : colors.neutral200} />
|
|
</Pressable>
|
|
</View>
|
|
) : null}
|
|
<Text style={styles.swipeHint}>다른 티켓 보기</Text>
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
backdrop: {
|
|
flex: 1,
|
|
backgroundColor: 'rgba(16,24,40,0.6)',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: spacing.lg,
|
|
},
|
|
sheet: {
|
|
width: '100%',
|
|
maxWidth: 360,
|
|
backgroundColor: colors.white,
|
|
borderRadius: radius.md,
|
|
overflow: 'hidden',
|
|
},
|
|
header: {
|
|
backgroundColor: colors.primary700,
|
|
paddingVertical: spacing.lg,
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
closeBtn: {
|
|
position: 'absolute',
|
|
top: 8,
|
|
right: 8,
|
|
width: touch.min,
|
|
height: touch.min,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
headerTitle: { color: colors.white, fontSize: type.h2.fontSize, fontWeight: '700' },
|
|
headerSub: { color: 'rgba(255,255,255,0.85)', fontSize: type.caption.fontSize, maxWidth: 260 },
|
|
content: { alignItems: 'center', padding: spacing.lg, gap: spacing.md },
|
|
nameRow: { flexDirection: 'row', alignItems: 'baseline' },
|
|
holderName: { fontSize: type.h1.fontSize, fontWeight: '700', color: colors.neutral900 },
|
|
holderType: { fontSize: type.body.fontSize, fontWeight: '500', color: colors.neutral500 },
|
|
bookingChip: {
|
|
backgroundColor: colors.primary050,
|
|
borderRadius: radius.pill,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 4,
|
|
},
|
|
bookingText: { color: colors.primary700, fontSize: type.caption.fontSize, fontVariant: ['tabular-nums'] },
|
|
hint: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
backgroundColor: colors.neutral050,
|
|
borderRadius: radius.md,
|
|
padding: spacing.md,
|
|
width: '100%',
|
|
},
|
|
hintText: { flex: 1, fontSize: type.caption.fontSize, color: colors.neutral700 },
|
|
hintBold: { fontWeight: '700', color: colors.neutral900 },
|
|
swipeRow: { flexDirection: 'row', alignItems: 'center', gap: 12, marginTop: 4 },
|
|
navBtn: {
|
|
width: touch.min,
|
|
height: touch.min,
|
|
borderRadius: radius.sm,
|
|
borderWidth: 1,
|
|
borderColor: colors.neutral200,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
navBtnOff: { opacity: 0.5 },
|
|
dots: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
dot: { width: 6, height: 6, borderRadius: 3, backgroundColor: colors.neutral200 },
|
|
dotOn: { width: 20, backgroundColor: colors.primary600 },
|
|
swipeHint: { fontSize: 11, color: colors.neutral500, letterSpacing: 1 },
|
|
});
|