kintex/mobile/app/(visitor)/index.tsx
zio 0933dbea41 feat(mobile): B2C visitor tab, congestion pill, live notice feed, ticket wallet, brand logo (v0.2.2 prep)
- (visitor) route group, visitor lib, tickets wallet
- CongestionPill + LiveNoticeFeed components
- login/tickets screens, i18n 4 locales, wordmark assets, splash

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 00:41:57 +09:00

140 lines
5.1 KiB
TypeScript

/*
* SCR-M5 계열 [모바일] 관람객(B2C) 홈 — 친근한 소비자 톤.
* 인사 + 활성 행사 요약 + 라이브 공지 피드(상단 요약 3건) + 빠른 이동 타일(티켓·행사·현장) + AI 관람 도우미 안내.
* design.md §4 SCR-M5. 라이브 공지 = 공개 API(cms_backlog_contract.md).
*/
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { Banner } from '../../components/Banner';
import { Card } from '../../components/Card';
import { DdayChip } from '../../components/DdayChip';
import { LiveNoticeFeed } from '../../components/LiveNoticeFeed';
import { useAuth } from '../../context/AuthContext';
import { DEFAULT_PUBLIC_EVENT_ID } from '../../lib/config';
import { colors, radius, spacing, type } from '../../theme';
export default function VisitorHomeScreen() {
const { t } = useTranslation();
const { user, activeWorkspace } = useAuth();
const eventId = activeWorkspace?.eventId ?? DEFAULT_PUBLIC_EVENT_ID;
const tiles: {
key: string;
icon: keyof typeof Ionicons.glyphMap;
label: string;
desc: string;
onPress: () => void;
}[] = [
{
key: 'tickets',
icon: 'ticket-outline',
label: t('vhome.tileTickets'),
desc: t('vhome.tileTicketsDesc'),
onPress: () => router.push('/(visitor)/tickets'),
},
{
key: 'events',
icon: 'calendar-outline',
label: t('vhome.tileEvents'),
desc: t('vhome.tileEventsDesc'),
onPress: () => router.push('/(visitor)/events'),
},
{
key: 'onsite',
icon: 'navigate-outline',
label: t('vhome.tileOnsite'),
desc: t('vhome.tileOnsiteDesc'),
onPress: () => router.push('/(visitor)/onsite'),
},
];
return (
<ScrollView contentContainerStyle={styles.scroll}>
<Text style={styles.hello}>
{t('vhome.hello', { name: user?.displayName ?? t('common.user') })}
</Text>
<Text style={styles.sub}>{t('vhome.subtitle')}</Text>
{/* 활성 행사 요약(참여 행사가 있을 때) */}
{activeWorkspace ? (
<Card accent="none">
<View style={styles.eventTop}>
<Text style={styles.eventName} numberOfLines={1}>
{activeWorkspace.eventName}
</Text>
<DdayChip dday={activeWorkspace.dday} />
</View>
<Text style={styles.eventMeta}>
{activeWorkspace.hallLabel} · {activeWorkspace.startDate} ~ {activeWorkspace.endDate}
</Text>
</Card>
) : (
<Banner tone="info">{t('vhome.noEvent')}</Banner>
)}
{/* 라이브 공지 요약(상단 3건) */}
<LiveNoticeFeed eventId={eventId} max={3} />
{/* 빠른 이동 타일 */}
<View style={styles.tiles}>
{tiles.map((tile) => (
<Pressable key={tile.key} style={styles.tile} onPress={tile.onPress}>
<View style={styles.tileIcon}>
<Ionicons name={tile.icon} size={24} color={colors.primary600} />
</View>
<Text style={styles.tileLabel}>{tile.label}</Text>
<Text style={styles.tileDesc}>{tile.desc}</Text>
</Pressable>
))}
</View>
{/* AI 관람 도우미 안내 */}
<Card accent="ai">
<View style={styles.aiRow}>
<Ionicons name="sparkles-outline" size={20} color={colors.aiAccent} />
<View style={{ flex: 1 }}>
<Text style={styles.aiTitle}>{t('vhome.aiTitle')}</Text>
<Text style={styles.aiDesc}>{t('vhome.aiDesc')}</Text>
</View>
</View>
</Card>
</ScrollView>
);
}
const styles = StyleSheet.create({
scroll: { padding: spacing.md, gap: spacing.md, paddingBottom: 40 },
hello: { fontSize: type.h2.fontSize, fontWeight: '700', color: colors.neutral900 },
sub: { marginTop: -8, fontSize: type.caption.fontSize, color: colors.neutral500 },
eventTop: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 },
eventName: { flex: 1, fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 },
eventMeta: { marginTop: 4, fontSize: type.caption.fontSize, color: colors.neutral500 },
tiles: { flexDirection: 'row', gap: spacing.sm },
tile: {
flex: 1,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.neutral200,
borderRadius: radius.md,
padding: spacing.sm,
gap: 6,
minHeight: 104,
},
tileIcon: {
width: 40,
height: 40,
borderRadius: radius.sm,
backgroundColor: colors.primary050,
alignItems: 'center',
justifyContent: 'center',
},
tileLabel: { fontSize: type.body.fontSize, fontWeight: '700', color: colors.neutral900 },
tileDesc: { fontSize: 11, color: colors.neutral500, lineHeight: 15 },
aiRow: { flexDirection: 'row', gap: 10, alignItems: 'flex-start' },
aiTitle: { fontSize: type.body.fontSize, fontWeight: '700', color: colors.aiAccent },
aiDesc: { marginTop: 2, fontSize: type.caption.fontSize, color: colors.neutral700, lineHeight: 18 },
});