363 lines
14 KiB
TypeScript
363 lines
14 KiB
TypeScript
/*
|
|
* SCR-M14 연계 [모바일] 티켓 권종 선택 — SCR-M15 예매 진입(보조 화면).
|
|
* 4단계 스텝(권종·예매자·결제·완료) 중 1단계. 권종 카드 + 수량 스테퍼 + 합계 스티키 바.
|
|
* ※ 결제는 PG 위임 — 카드번호 입력 UI 없음(design.md 보안 불변). M10 미구현 → 샘플 권종.
|
|
* "다음"은 후속 단계(예매자/결제) 미구현 안내(존재하지 않는 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 { colors, radius, spacing, touch, type } from '../../theme';
|
|
|
|
interface TicketType {
|
|
id: string;
|
|
name: string;
|
|
price: number; // 원(0=무료)
|
|
priceNote?: string;
|
|
desc?: string;
|
|
badge?: { text: string; tone: 'discount' | 'ai' };
|
|
soldOut?: boolean;
|
|
accent: string;
|
|
}
|
|
|
|
const TICKET_TYPES: TicketType[] = [
|
|
{ id: 'std', name: '일반권', price: 0, priceNote: '사전등록 할인가', accent: colors.primary600 },
|
|
{
|
|
id: 'buyer',
|
|
name: '바이어권',
|
|
price: 0,
|
|
desc: '무료 (자격심사 필요)',
|
|
badge: { text: 'AI 추천', tone: 'ai' },
|
|
accent: colors.aiAccent,
|
|
},
|
|
{
|
|
id: 'group',
|
|
name: '단체권',
|
|
price: 9000,
|
|
priceNote: '10인 이상 단체 구매시 적용',
|
|
badge: { text: '10% OFF', tone: 'discount' },
|
|
accent: colors.success,
|
|
},
|
|
{ id: 'vip', name: 'VIP권', price: 50000, soldOut: true, accent: colors.neutral500 },
|
|
];
|
|
|
|
const STEPS = ['권종', '예매자', '결제', '완료'];
|
|
|
|
function won(n: number): string {
|
|
return n === 0 ? '₩0' : `₩${n.toLocaleString('ko-KR')}`;
|
|
}
|
|
|
|
export default function TicketSelectScreen() {
|
|
const [qty, setQty] = useState<Record<string, number>>({ group: 1 });
|
|
|
|
const totalCount = useMemo(
|
|
() => Object.values(qty).reduce((a, b) => a + b, 0),
|
|
[qty],
|
|
);
|
|
const totalPrice = useMemo(
|
|
() => TICKET_TYPES.reduce((sum, t) => sum + (qty[t.id] ?? 0) * t.price, 0),
|
|
[qty],
|
|
);
|
|
|
|
function change(id: string, delta: number) {
|
|
setQty((q) => ({ ...q, [id]: Math.max(0, (q[id] ?? 0) + delta) }));
|
|
}
|
|
|
|
function next() {
|
|
if (totalCount === 0) return;
|
|
Alert.alert(
|
|
'다음 단계',
|
|
'예매자 정보·결제 단계는 티켓 백엔드(M10) 및 PG 어댑터 연동 후 제공됩니다.\n결제는 PG사 보안 페이지에서 진행되며 카드정보는 저장되지 않습니다.',
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.flex}>
|
|
<Stack.Screen options={{ title: '티켓 예매', headerTitleAlign: 'center' }} />
|
|
|
|
<ScrollView contentContainerStyle={styles.scroll}>
|
|
{/* 행사 요약 칩 */}
|
|
<View style={styles.eventChip}>
|
|
<Ionicons name="calendar-outline" size={16} color={colors.primary600} />
|
|
<Text style={styles.eventChipText} numberOfLines={1}>
|
|
스마트팩토리 코리아 2026 · 제2전시장 홀7
|
|
</Text>
|
|
</View>
|
|
|
|
{/* 스텝 인디케이터 */}
|
|
<View style={styles.steps}>
|
|
{STEPS.map((s, i) => (
|
|
<React.Fragment key={s}>
|
|
<View style={styles.step}>
|
|
<View style={[styles.stepDot, i === 0 && styles.stepDotOn]}>
|
|
<Text style={[styles.stepNum, i === 0 && styles.stepNumOn]}>{i + 1}</Text>
|
|
</View>
|
|
<Text style={[styles.stepLabel, i === 0 && styles.stepLabelOn]}>{s}</Text>
|
|
</View>
|
|
{i < STEPS.length - 1 ? <View style={styles.stepLine} /> : null}
|
|
</React.Fragment>
|
|
))}
|
|
</View>
|
|
|
|
{/* 권종 카드 */}
|
|
<View style={styles.cardList}>
|
|
{TICKET_TYPES.map((t) => {
|
|
const count = qty[t.id] ?? 0;
|
|
return (
|
|
<View
|
|
key={t.id}
|
|
style={[styles.card, t.soldOut && styles.cardSoldOut]}
|
|
>
|
|
<View style={[styles.cardAccent, { backgroundColor: t.accent }]} />
|
|
<View style={styles.cardBody}>
|
|
<View style={styles.cardTop}>
|
|
<View style={styles.cardInfo}>
|
|
<View style={styles.nameRow}>
|
|
<Text style={styles.typeName}>{t.name}</Text>
|
|
{t.badge ? (
|
|
<View
|
|
style={[
|
|
styles.typeBadge,
|
|
t.badge.tone === 'ai' ? styles.badgeAi : styles.badgeDiscount,
|
|
]}
|
|
>
|
|
{t.badge.tone === 'ai' ? (
|
|
<Ionicons name="sparkles" size={11} color={colors.aiAccent} />
|
|
) : null}
|
|
<Text
|
|
style={[
|
|
styles.typeBadgeText,
|
|
{ color: t.badge.tone === 'ai' ? colors.aiAccent : colors.error },
|
|
]}
|
|
>
|
|
{t.badge.text}
|
|
</Text>
|
|
</View>
|
|
) : null}
|
|
{t.soldOut ? (
|
|
<View style={styles.soldOutBadge}>
|
|
<Text style={styles.soldOutText}>매진</Text>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
{t.desc ? (
|
|
<Text style={styles.typeDesc}>{t.desc}</Text>
|
|
) : (
|
|
<Text style={styles.typePrice}>{won(t.price)}</Text>
|
|
)}
|
|
{t.priceNote ? <Text style={styles.priceNote}>{t.priceNote}</Text> : null}
|
|
</View>
|
|
|
|
{/* 수량 스테퍼 */}
|
|
<View style={styles.stepper}>
|
|
<Pressable
|
|
accessibilityLabel="감소"
|
|
disabled={t.soldOut}
|
|
onPress={() => change(t.id, -1)}
|
|
style={styles.stepperBtn}
|
|
>
|
|
<Ionicons
|
|
name="remove"
|
|
size={20}
|
|
color={t.soldOut ? colors.neutral200 : colors.neutral700}
|
|
/>
|
|
</Pressable>
|
|
<Text style={styles.stepperVal}>{count}</Text>
|
|
<Pressable
|
|
accessibilityLabel="증가"
|
|
disabled={t.soldOut}
|
|
onPress={() => change(t.id, 1)}
|
|
style={styles.stepperBtn}
|
|
>
|
|
<Ionicons
|
|
name="add"
|
|
size={20}
|
|
color={t.soldOut ? colors.neutral200 : colors.neutral700}
|
|
/>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
{t.badge?.tone === 'ai' ? (
|
|
<Text style={styles.aiNote}>
|
|
※ 업종 및 직무 AI 매칭을 통해 추천된 권종입니다.
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
|
|
{/* 결제 수단 미리보기 */}
|
|
<View style={styles.paySection}>
|
|
<Text style={styles.payTitle}>결제 수단 미리보기</Text>
|
|
<View style={styles.payChips}>
|
|
{['신용카드', '간편결제', '계좌이체'].map((p) => (
|
|
<View key={p} style={styles.payChip}>
|
|
<Text style={styles.payChipText}>{p}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
<View style={styles.secureNote}>
|
|
<Ionicons name="shield-checkmark-outline" size={20} color={colors.neutral500} />
|
|
<Text style={styles.secureText}>
|
|
결제는 PG사 보안 페이지에서 진행되며 카드정보는 저장되지 않습니다.
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</ScrollView>
|
|
|
|
{/* 하단 스티키 합계 바 */}
|
|
<View style={styles.footer}>
|
|
<View style={styles.totalRow}>
|
|
<Text style={styles.totalLabel}>선택한 티켓 {totalCount}매</Text>
|
|
<Text style={styles.totalPrice}>합계 {won(totalPrice)}</Text>
|
|
</View>
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
disabled={totalCount === 0}
|
|
onPress={next}
|
|
style={[styles.nextBtn, totalCount === 0 && styles.nextBtnOff]}
|
|
>
|
|
<Text style={styles.nextText}>다음</Text>
|
|
<Ionicons name="chevron-forward" size={20} color={colors.white} />
|
|
</Pressable>
|
|
<Pressable style={styles.backLink} onPress={() => router.back()}>
|
|
<Text style={styles.backLinkText}>내 티켓 지갑으로 돌아가기</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
flex: { flex: 1, backgroundColor: colors.neutral050 },
|
|
scroll: { padding: spacing.md, gap: spacing.md, paddingBottom: 200 },
|
|
eventChip: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
alignSelf: 'flex-start',
|
|
backgroundColor: colors.primary050,
|
|
borderRadius: radius.pill,
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 8,
|
|
maxWidth: '100%',
|
|
},
|
|
eventChipText: { flex: 1, fontSize: type.caption.fontSize, color: colors.neutral900, fontWeight: '600' },
|
|
steps: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 4 },
|
|
step: { alignItems: 'center', gap: 4 },
|
|
stepDot: {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
backgroundColor: colors.neutral200,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
stepDotOn: { backgroundColor: colors.primary600 },
|
|
stepNum: { fontSize: type.body.fontSize, fontWeight: '700', color: colors.neutral500 },
|
|
stepNumOn: { color: colors.white },
|
|
stepLabel: { fontSize: 11, color: colors.neutral500 },
|
|
stepLabelOn: { color: colors.primary700, fontWeight: '700' },
|
|
stepLine: { flex: 1, height: 1, backgroundColor: colors.neutral200, marginHorizontal: 6, marginBottom: 18 },
|
|
cardList: { gap: spacing.md },
|
|
card: {
|
|
flexDirection: 'row',
|
|
backgroundColor: colors.white,
|
|
borderWidth: 1,
|
|
borderColor: colors.neutral200,
|
|
borderRadius: radius.md,
|
|
overflow: 'hidden',
|
|
},
|
|
cardSoldOut: { opacity: 0.6 },
|
|
cardAccent: { width: 4 },
|
|
cardBody: { flex: 1, padding: spacing.md, gap: 8 },
|
|
cardTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 },
|
|
cardInfo: { flex: 1, gap: 4 },
|
|
nameRow: { flexDirection: 'row', alignItems: 'center', gap: 8, flexWrap: 'wrap' },
|
|
typeName: { fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 },
|
|
typeBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 3,
|
|
borderRadius: radius.sm,
|
|
paddingHorizontal: 6,
|
|
paddingVertical: 2,
|
|
borderWidth: 1,
|
|
},
|
|
badgeAi: { borderColor: colors.aiAccent, backgroundColor: colors.aiSurface },
|
|
badgeDiscount: { borderColor: colors.error, backgroundColor: '#FEF3F2' },
|
|
typeBadgeText: { fontSize: 10, fontWeight: '700' },
|
|
soldOutBadge: { backgroundColor: colors.neutral500, borderRadius: radius.sm, paddingHorizontal: 6, paddingVertical: 2 },
|
|
soldOutText: { color: colors.white, fontSize: 10, fontWeight: '700' },
|
|
typePrice: { fontSize: type.h2.fontSize, fontWeight: '700', color: colors.neutral900 },
|
|
typeDesc: { fontSize: type.body.fontSize, color: colors.neutral700 },
|
|
priceNote: { fontSize: type.caption.fontSize, color: colors.neutral500 },
|
|
aiNote: { fontSize: 11, color: colors.aiAccent, fontWeight: '500' },
|
|
stepper: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: colors.neutral050,
|
|
borderRadius: radius.sm,
|
|
borderWidth: 1,
|
|
borderColor: colors.neutral200,
|
|
},
|
|
stepperBtn: { width: touch.min, height: touch.min, alignItems: 'center', justifyContent: 'center' },
|
|
stepperVal: { width: 28, textAlign: 'center', fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 },
|
|
paySection: { gap: 8, marginTop: 4 },
|
|
payTitle: { fontSize: type.caption.fontSize, color: colors.neutral500, fontWeight: '600' },
|
|
payChips: { flexDirection: 'row', gap: 8 },
|
|
payChip: {
|
|
backgroundColor: colors.white,
|
|
borderWidth: 1,
|
|
borderColor: colors.neutral200,
|
|
borderRadius: radius.pill,
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 8,
|
|
},
|
|
payChipText: { fontSize: type.caption.fontSize, color: colors.neutral700 },
|
|
secureNote: {
|
|
flexDirection: 'row',
|
|
gap: 10,
|
|
alignItems: 'flex-start',
|
|
backgroundColor: colors.white,
|
|
borderWidth: 1,
|
|
borderColor: colors.neutral200,
|
|
borderStyle: 'dashed',
|
|
borderRadius: radius.md,
|
|
padding: spacing.md,
|
|
marginTop: 8,
|
|
},
|
|
secureText: { flex: 1, fontSize: type.caption.fontSize, color: colors.neutral700, lineHeight: 18 },
|
|
footer: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
backgroundColor: colors.white,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.neutral200,
|
|
padding: spacing.md,
|
|
paddingBottom: spacing.lg,
|
|
gap: 10,
|
|
},
|
|
totalRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
|
totalLabel: { fontSize: type.body.fontSize, color: colors.neutral500 },
|
|
totalPrice: { fontSize: type.h2.fontSize, fontWeight: '700', color: colors.primary700 },
|
|
nextBtn: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 6,
|
|
minHeight: 56,
|
|
backgroundColor: colors.primary600,
|
|
borderRadius: radius.md,
|
|
},
|
|
nextBtnOff: { opacity: 0.5 },
|
|
nextText: { color: colors.white, fontSize: type.h2.fontSize, fontWeight: '700' },
|
|
backLink: { alignItems: 'center', minHeight: 32, justifyContent: 'center' },
|
|
backLinkText: { fontSize: type.caption.fontSize, color: colors.neutral500, textDecorationLine: 'underline' },
|
|
});
|