/* * SCR-M15 [모바일] 내 티켓 지갑 — 관람객(B2C) 트랙. * 필터 탭(진행중·예정·지난) + 티켓 카드 리스트 + QR 풀스크린 뷰어. * 배지 전환 안내 배너 + 오프라인 표시 배지. 사용됨/취소 카드 흐리게. * ※ M10(티켓)·M9(환불) 백엔드 미구현 → 샘플 데이터("샘플" 배지). API 호출 없음. */ import { Ionicons } from '@expo/vector-icons'; import { router, Stack } from 'expo-router'; import React, { useMemo, useState } from 'react'; import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { Banner } from '../../components/Banner'; import { QrViewerModal } from '../../components/tickets/QrViewerModal'; import { TicketCard } from '../../components/tickets/TicketCard'; import { FILTER_TABS, SAMPLE_TICKETS, type SampleTicket, type TicketFilter, } from '../../components/tickets/sampleTickets'; import { colors, radius, spacing, type } from '../../theme'; export default function TicketWalletScreen() { const [filter, setFilter] = useState('active'); const [qrOpen, setQrOpen] = useState(false); const [qrIndex, setQrIndex] = useState(0); const filtered = useMemo( () => SAMPLE_TICKETS.filter((t) => t.filter === filter), [filter], ); // QR 뷰어 대상 = 현재 필터 내 사용가능 티켓만(스와이프 전환 범위) const usableInView = useMemo( () => filtered.filter((t) => t.status === 'usable'), [filtered], ); function openQr(t: SampleTicket) { const i = usableInView.findIndex((x) => x.id === t.id); setQrIndex(i < 0 ? 0 : i); setQrOpen(true); } function openDetail(t: SampleTicket) { // SCR-P8(예매 확인·취소) 미구현 → 안내만(샘플) Alert.alert( '예매 상세·취소', `${t.eventName}\n예매번호 ${t.bookingNoMasked}\n\n예매 확인·취소 화면(SCR-P8)은 티켓 백엔드(M10) 연동 후 제공됩니다.`, ); } return ( {/* 오프라인 표시 배지 */} 오프라인 — 저장된 티켓 표시 중 {/* 필터 탭(세그먼트) */} {FILTER_TABS.map((tab) => { const on = filter === tab.key; return ( setFilter(tab.key)} > {tab.label} ); })} {/* 배지 전환 안내 배너 */} 티켓 QR로 현장 체크인하면 모바일 배지로 전환됩니다 {/* 티켓 리스트 / 빈 상태 */} {filtered.length === 0 ? ( ) : ( filtered.map((t) => ( )) )} 표시된 티켓은 샘플입니다 — 티켓 백엔드(M10) 연동 시 실데이터로 대체됩니다. setQrOpen(false)} /> ); } function EmptyState() { return ( 보유한 티켓이 없습니다 입장권을 예매하면 이곳에 티켓이 표시됩니다. router.push('/tickets/select')} > 입장권 예매 ); } const styles = StyleSheet.create({ flex: { flex: 1, backgroundColor: colors.neutral050 }, scroll: { padding: spacing.md, gap: spacing.md, paddingBottom: 40 }, offlineWrap: { alignItems: 'center' }, offlineBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: colors.white, borderWidth: 1, borderColor: colors.neutral200, borderRadius: radius.pill, paddingHorizontal: 12, paddingVertical: 5, }, offlineDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: colors.primary600 }, offlineText: { fontSize: 11, color: colors.neutral500, fontWeight: '500' }, segment: { flexDirection: 'row', backgroundColor: colors.white, borderWidth: 1, borderColor: colors.neutral200, borderRadius: radius.md, padding: 4, gap: 4, }, segBtn: { flex: 1, minHeight: 40, alignItems: 'center', justifyContent: 'center', borderRadius: radius.sm, }, segBtnOn: { backgroundColor: colors.primary050 }, segText: { fontSize: type.body.fontSize, fontWeight: '600', color: colors.neutral500 }, segTextOn: { color: colors.primary700 }, footNote: { textAlign: 'center', fontSize: 11, color: colors.neutral500, marginTop: 4 }, empty: { alignItems: 'center', gap: 8, paddingVertical: spacing.xl, backgroundColor: colors.white, borderWidth: 1, borderColor: colors.neutral200, borderRadius: radius.md, }, emptyTitle: { fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 }, emptyBody: { fontSize: type.caption.fontSize, color: colors.neutral500, textAlign: 'center' }, emptyBtn: { marginTop: 8, minHeight: 48, paddingHorizontal: 24, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.primary600, borderRadius: radius.sm, }, emptyBtnText: { color: colors.white, fontSize: type.h3.fontSize, fontWeight: '700' }, });