From 0933dbea4178b056a511d822d1bdace67455072c Mon Sep 17 00:00:00 2001 From: zio Date: Fri, 24 Jul 2026 00:41:57 +0900 Subject: [PATCH] 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 --- mobile/app.json | 4 +- mobile/app/(visitor)/_layout.tsx | 69 +++ mobile/app/(visitor)/events.tsx | 94 +++ mobile/app/(visitor)/index.tsx | 139 +++++ mobile/app/(visitor)/my.tsx | 177 ++++++ mobile/app/(visitor)/onsite.tsx | 672 +++++++++++++++++++++ mobile/app/(visitor)/tickets.tsx | 10 + mobile/app/_layout.tsx | 1 + mobile/app/login.tsx | 13 +- mobile/app/tickets/index.tsx | 189 +----- mobile/assets/logo-wordmark-white.png | Bin 0 -> 4335 bytes mobile/assets/logo-wordmark.png | Bin 0 -> 15760 bytes mobile/assets/splash.png | Bin 31218 -> 55576 bytes mobile/components/CongestionPill.tsx | 51 ++ mobile/components/LiveNoticeFeed.tsx | 173 ++++++ mobile/components/tickets/TicketWallet.tsx | 177 ++++++ mobile/lib/config.ts | 7 + mobile/lib/i18n/locales/en.ts | 87 +++ mobile/lib/i18n/locales/ja.ts | 87 +++ mobile/lib/i18n/locales/ko.ts | 87 +++ mobile/lib/i18n/locales/zh.ts | 87 +++ mobile/lib/roleTrack.ts | 4 +- mobile/lib/types.ts | 91 +++ mobile/lib/visitor.ts | 47 ++ 24 files changed, 2078 insertions(+), 188 deletions(-) create mode 100644 mobile/app/(visitor)/_layout.tsx create mode 100644 mobile/app/(visitor)/events.tsx create mode 100644 mobile/app/(visitor)/index.tsx create mode 100644 mobile/app/(visitor)/my.tsx create mode 100644 mobile/app/(visitor)/onsite.tsx create mode 100644 mobile/app/(visitor)/tickets.tsx create mode 100644 mobile/assets/logo-wordmark-white.png create mode 100644 mobile/assets/logo-wordmark.png create mode 100644 mobile/components/CongestionPill.tsx create mode 100644 mobile/components/LiveNoticeFeed.tsx create mode 100644 mobile/components/tickets/TicketWallet.tsx create mode 100644 mobile/lib/visitor.ts diff --git a/mobile/app.json b/mobile/app.json index 21dd44e..73bb946 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -4,7 +4,7 @@ "slug": "kintex", "owner": "zioinfo", "scheme": "kintex", - "version": "0.2.0", + "version": "0.2.2", "orientation": "portrait", "userInterfaceStyle": "light", "jsEngine": "hermes", @@ -28,7 +28,7 @@ }, "android": { "package": "kr.co.zioinfo.kintex", - "versionCode": 2, + "versionCode": 4, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#0066B3" diff --git a/mobile/app/(visitor)/_layout.tsx b/mobile/app/(visitor)/_layout.tsx new file mode 100644 index 0000000..0da9a57 --- /dev/null +++ b/mobile/app/(visitor)/_layout.tsx @@ -0,0 +1,69 @@ +/* + * 관람객(B2C) 하단 탭 셸 — 운영(B2B) (tabs) 셸과 분리된 관람객 전용 트랙. + * 탭: 홈 · 행사 · 티켓 · 현장(혼잡/주차) · 마이. design.md §4 SCR-M5/M14/M15 관람객 톤. + * 진입: roleTrack visitor 랜딩(/(visitor)) 또는 게스트. 인증 가드는 (tabs)와 동일(로그인 필요). + */ +import { Ionicons } from '@expo/vector-icons'; +import { Redirect, Tabs } from 'expo-router'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from '../../context/AuthContext'; +import { colors } from '../../theme'; + +export default function VisitorTabsLayout() { + const { t } = useTranslation(); + const { ready, token } = useAuth(); + if (ready && !token) return ; + + return ( + + , + }} + /> + ( + + ), + }} + /> + , + }} + /> + ( + + ), + }} + /> + , + }} + /> + + ); +} diff --git a/mobile/app/(visitor)/events.tsx b/mobile/app/(visitor)/events.tsx new file mode 100644 index 0000000..6950eaf --- /dev/null +++ b/mobile/app/(visitor)/events.tsx @@ -0,0 +1,94 @@ +/* + * [모바일] 관람객(B2C) 행사 — 참여 행사 목록 + 선택 행사 라이브 공지 피드(전체). + * 참여 행사는 로그인 워크스페이스에서 수신(계약 발명 없음). 없으면 데모 라이브 행사로 공개 공지 노출. + * 라이브 공지 = 공개 API(cms_backlog_contract.md). + */ +import React, { useState } 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 VisitorEventsScreen() { + const { t } = useTranslation(); + const { workspaces, activeWorkspace } = useAuth(); + const [selected, setSelected] = useState( + activeWorkspace?.eventId ?? workspaces[0]?.eventId ?? null, + ); + + const selectedWs = workspaces.find((w) => w.eventId === selected) ?? null; + const feedEventId = selected ?? DEFAULT_PUBLIC_EVENT_ID; + + return ( + + {t('vevents.title')} + + {workspaces.length === 0 ? ( + {t('vevents.empty')} + ) : ( + <> + {/* 참여 행사 선택 칩 */} + + {workspaces.map((w) => { + const on = w.eventId === selected; + return ( + setSelected(w.eventId)} + style={[styles.chip, on && styles.chipOn]} + > + + {w.eventName} + + + ); + })} + + + {/* 선택 행사 요약 카드 */} + {selectedWs ? ( + + + + {selectedWs.eventName} + + + + + {selectedWs.hallLabel} · {selectedWs.startDate} ~ {selectedWs.endDate} + + + ) : null} + + )} + + {/* 라이브 공지(전체) */} + + + ); +} + +const styles = StyleSheet.create({ + scroll: { padding: spacing.md, gap: spacing.md, paddingBottom: 40 }, + title: { fontSize: type.h2.fontSize, fontWeight: '700', color: colors.neutral900 }, + chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, + chip: { + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: radius.pill, + borderWidth: 1, + borderColor: colors.neutral200, + maxWidth: 240, + }, + chipOn: { backgroundColor: colors.primary100, borderColor: colors.primary600 }, + chipText: { color: colors.neutral700, fontSize: type.caption.fontSize }, + chipTextOn: { color: colors.primary700, fontWeight: '700' }, + eventTop: { flexDirection: 'row', alignItems: 'flex-start', 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 }, +}); diff --git a/mobile/app/(visitor)/index.tsx b/mobile/app/(visitor)/index.tsx new file mode 100644 index 0000000..653eeff --- /dev/null +++ b/mobile/app/(visitor)/index.tsx @@ -0,0 +1,139 @@ +/* + * 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 ( + + + {t('vhome.hello', { name: user?.displayName ?? t('common.user') })} + + {t('vhome.subtitle')} + + {/* 활성 행사 요약(참여 행사가 있을 때) */} + {activeWorkspace ? ( + + + + {activeWorkspace.eventName} + + + + + {activeWorkspace.hallLabel} · {activeWorkspace.startDate} ~ {activeWorkspace.endDate} + + + ) : ( + {t('vhome.noEvent')} + )} + + {/* 라이브 공지 요약(상단 3건) */} + + + {/* 빠른 이동 타일 */} + + {tiles.map((tile) => ( + + + + + {tile.label} + {tile.desc} + + ))} + + + {/* AI 관람 도우미 안내 */} + + + + + {t('vhome.aiTitle')} + {t('vhome.aiDesc')} + + + + + ); +} + +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 }, +}); diff --git a/mobile/app/(visitor)/my.tsx b/mobile/app/(visitor)/my.tsx new file mode 100644 index 0000000..86c410b --- /dev/null +++ b/mobile/app/(visitor)/my.tsx @@ -0,0 +1,177 @@ +/* + * [모바일] 관람객(B2C) 마이 — 내정보·내 티켓·내 주차권·언어·서버 상태·로그아웃. + * 운영(B2B) 더보기(SCR-M3 more)의 관람객 경량판. 공통 키(more.*) 재사용 + vmy.* 최소 신설. + */ +import { Ionicons } from '@expo/vector-icons'; +import { router } from 'expo-router'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { Avatar } from '../../components/Avatar'; +import { Card } from '../../components/Card'; +import { LanguageSelector } from '../../components/LanguageSelector'; +import { useAuth } from '../../context/AuthContext'; +import { api, isDegraded } from '../../lib/api'; +import { API_BASE } from '../../lib/config'; +import type { HealthDto } from '../../lib/types'; +import { colors, radius, spacing, touch, type } from '../../theme'; + +export default function VisitorMyScreen() { + const { t } = useTranslation(); + const { user, signOut } = useAuth(); + const [health, setHealth] = useState(t('common.loading')); + + const checkHealth = useCallback(async () => { + try { + const res = await api.get('/health', { anonymous: true }); + setHealth(res?.status === 'UP' ? t('more.statusUp') : (res?.status ?? t('common.unknown'))); + } catch (e) { + setHealth(isDegraded(e) ? t('more.statusDown') : t('common.error')); + } + }, [t]); + + useEffect(() => { + checkHealth(); + }, [checkHealth]); + + async function onSignOut() { + await signOut(); + router.replace('/login'); + } + + return ( + + {/* 내 정보 진입 */} + router.push('/profile')} + accessibilityRole="button" + accessibilityLabel={t('more.openProfile')} + > + + + + + {user?.displayName ?? t('common.user')} + {t('more.accountGeneral')} + + + + + + + {/* 바로가기 — 내 티켓 · 내 주차권 */} + router.push('/(visitor)/tickets')} + /> + router.push('/(visitor)/onsite')} + /> + + {/* 언어 */} + + + {/* 서버 상태 */} + + + + + + + + + + + + {t('more.signOut')} + + + {t('more.version')} + + ); +} + +function NavRow({ + icon, + title, + desc, + onPress, +}: { + icon: keyof typeof Ionicons.glyphMap; + title: string; + desc: string; + onPress: () => void; +}) { + return ( + + + + + + {title} + {desc} + + + + ); +} + +function Row({ label, value }: { label: string; value: string }) { + return ( + + {label} + + {value} + + + ); +} + +const styles = StyleSheet.create({ + scroll: { padding: spacing.md, gap: spacing.md, paddingBottom: 40 }, + meRow: { flexDirection: 'row', alignItems: 'center' }, + name: { fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 }, + meta: { marginTop: 4, color: colors.neutral500, fontSize: type.caption.fontSize }, + navRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.neutral200, + borderRadius: radius.md, + padding: spacing.md, + }, + navIcon: { + width: 40, + height: 40, + borderRadius: radius.sm, + backgroundColor: colors.primary050, + alignItems: 'center', + justifyContent: 'center', + }, + navTitle: { fontSize: type.h3.fontSize, fontWeight: '600', color: colors.neutral900 }, + navDesc: { fontSize: type.caption.fontSize, color: colors.neutral500 }, + row: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, paddingVertical: 4 }, + rowLabel: { color: colors.neutral500, fontSize: type.body.fontSize }, + rowValue: { flex: 1, textAlign: 'right', color: colors.neutral900, fontSize: type.body.fontSize }, + divider: { height: 1, backgroundColor: colors.neutral200, marginVertical: 6 }, + healthRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, + signOut: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + minHeight: touch.min, + borderRadius: radius.sm, + borderWidth: 1, + borderColor: colors.error, + }, + signOutText: { color: colors.error, fontSize: type.h3.fontSize, fontWeight: '600' }, + version: { textAlign: 'center', color: colors.neutral500, fontSize: type.caption.fontSize }, +}); diff --git a/mobile/app/(visitor)/onsite.tsx b/mobile/app/(visitor)/onsite.tsx new file mode 100644 index 0000000..111a21f --- /dev/null +++ b/mobile/app/(visitor)/onsite.tsx @@ -0,0 +1,672 @@ +/* + * F-C2/F-C3 [모바일] 관람객(B2C) 현장 — 실시간 혼잡 + 주차 현황 + 사전 주차권(mock). + * 세그먼트: 혼잡도 | 주차. 혼잡/주차 현황은 공개 조회, 주차권은 인증(본인). + * 계약: _workspace/parking_congestion_contract.md. PII(차량번호)는 서버 마스킹 — 원문 미저장. + */ +import { Ionicons } from '@expo/vector-icons'; +import { router } from 'expo-router'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + ActivityIndicator, + Modal, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import { Banner } from '../../components/Banner'; +import { CongestionPill } from '../../components/CongestionPill'; +import { useAuth } from '../../context/AuthContext'; +import { isDegraded } from '../../lib/api'; +import { DEFAULT_PUBLIC_EVENT_ID } from '../../lib/config'; +import type { + CongestionAreaDto, + CongestionOverviewDto, + ParkingLotStatusDto, + ParkingPassDto, +} from '../../lib/types'; +import { + getCongestion, + getMyParkingPasses, + getParkingLots, + purchaseParkingPass, +} from '../../lib/visitor'; +import { colors, radius, spacing, touch, type } from '../../theme'; + +type Seg = 'congestion' | 'parking'; + +function won(n: number): string { + return `${n.toLocaleString('ko-KR')}`; +} +function ymd(d: Date): string { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} + +export default function VisitorOnsiteScreen() { + const { t } = useTranslation(); + const { token, activeWorkspace } = useAuth(); + const eventId = activeWorkspace?.eventId ?? DEFAULT_PUBLIC_EVENT_ID; + + const [seg, setSeg] = useState('congestion'); + + return ( + + {/* 세그먼트 */} + + {(['congestion', 'parking'] as Seg[]).map((s) => { + const on = seg === s; + return ( + setSeg(s)} + > + + {t(s === 'congestion' ? 'onsite.segCongestion' : 'onsite.segParking')} + + + ); + })} + + + {seg === 'congestion' ? ( + + ) : ( + + )} + + ); +} + +// ── 혼잡도 ── +function CongestionView({ eventId }: { eventId: string }) { + const { t } = useTranslation(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [degraded, setDegraded] = useState(false); + + const load = useCallback(async () => { + try { + const res = await getCongestion(eventId); + setData(res); + setDegraded(false); + } catch (e) { + setDegraded(isDegraded(e)); + setData(null); + } finally { + setLoading(false); + } + }, [eventId]); + + useEffect(() => { + load(); + }, [load]); + + if (loading) { + return ( + + + + ); + } + if (!data) { + return ( + + + {degraded ? t('congestion.degraded') : t('congestion.empty')} + + + ); + } + + return ( + + {/* 종합 */} + + + {t('congestion.overall')} + + + + + + {t('congestion.onSite')} {t('congestion.people', { n: won(data.onSiteCount) })} + + + + + + + + + ); +} + +function AreaGroup({ + title, + icon, + areas, +}: { + title: string; + icon: keyof typeof Ionicons.glyphMap; + areas: CongestionAreaDto[]; +}) { + if (!areas || areas.length === 0) return null; + return ( + + + + {title} + + {areas.map((a) => ( + + + {a.label} + + + + ))} + + ); +} + +// ── 주차 ── +function ParkingView({ eventId, hasToken }: { eventId: string; hasToken: boolean }) { + const { t } = useTranslation(); + const [lots, setLots] = useState(null); + const [degraded, setDegraded] = useState(false); + const [passes, setPasses] = useState(null); + const [purchaseFor, setPurchaseFor] = useState(null); + + const loadLots = useCallback(async () => { + try { + const res = await getParkingLots(eventId); + setLots(res ?? []); + setDegraded(false); + } catch (e) { + setDegraded(isDegraded(e)); + setLots([]); + } + }, [eventId]); + + const loadPasses = useCallback(async () => { + if (!hasToken) { + setPasses(null); + return; + } + try { + const res = await getMyParkingPasses(); + setPasses(res?.passes ?? []); + } catch { + setPasses([]); // degraded/미배포 시 빈 목록(정직) + } + }, [hasToken]); + + useEffect(() => { + loadLots(); + loadPasses(); + }, [loadLots, loadPasses]); + + return ( + + {/* 주차 현황 */} + + + {t('parking.title')} + + + {lots == null ? ( + + + + ) : lots.length === 0 ? ( + + {degraded ? t('parking.degraded') : t('parking.empty')} + + ) : ( + lots.map((lot) => ( + setPurchaseFor(lot)} canBuy={hasToken} /> + )) + )} + + {/* 내 주차권 */} + + + {t('parking.myPasses')} + + {!hasToken ? ( + router.push('/login')}> + {t('parking.loginRequired')} + + + ) : passes == null ? ( + + + + ) : passes.length === 0 ? ( + {t('parking.noPasses')} + ) : ( + passes.map((p) => ) + )} + + setPurchaseFor(null)} + onPurchased={() => { + setPurchaseFor(null); + loadPasses(); + loadLots(); + }} + eventId={eventId} + /> + + ); +} + +function LotCard({ + lot, + onBuy, + canBuy, +}: { + lot: ParkingLotStatusDto; + onBuy: () => void; + canBuy: boolean; +}) { + const { t } = useTranslation(); + const pct = Math.max(0, Math.min(100, lot.occupancyPercent)); + return ( + + + + {lot.name} + + + + {/* 점유율 바 */} + + + + + + {t('parking.available', { n: won(lot.available) })} · {t('parking.capacity', { n: won(lot.totalCapacity) })} + + + + + {t('parking.rate', { n: won(lot.hourlyRate) })} + {lot.dailyMax != null ? ` · ${t('parking.daily', { n: won(lot.dailyMax) })}` : ''} + + + + + + {t('parking.buyPass')} · {t('parking.passPrice', { n: won(lot.passPrice) })} + + + {!canBuy ? {t('parking.loginRequired')} : null} + + ); +} + +const PASS_STATUS_TONE: Record = { + PAID: { color: colors.success, bg: '#E7F6EF' }, + USED: { color: colors.neutral700, bg: colors.neutral050 }, + CANCELLED: { color: colors.error, bg: '#FEF3F2' }, +}; + +function PassCard({ pass }: { pass: ParkingPassDto }) { + const { t } = useTranslation(); + const tone = PASS_STATUS_TONE[pass.status] ?? PASS_STATUS_TONE.USED; + const dim = pass.status !== 'PAID'; + return ( + + + + {pass.lotName} + + + + {t(`parking.passStatus.${pass.status}` as const)} + + + + + {pass.useDate} · {pass.passNo} + + + + {pass.vehiclePlateMasked ?? '-'} + + {t('parking.passPrice', { n: won(pass.amount) })} + + + ); +} + +function PurchaseModal({ + lot, + eventId, + onClose, + onPurchased, +}: { + lot: ParkingLotStatusDto | null; + eventId: string; + onClose: () => void; + onPurchased: () => void; +}) { + const { t } = useTranslation(); + const [dayOffset, setDayOffset] = useState(1); // 내일 기본(오늘 이후) + const [plate, setPlate] = useState(''); + const [busy, setBusy] = useState(false); + const [err, setErr] = useState(null); + + const useDate = useMemo(() => { + const d = new Date(); + d.setDate(d.getDate() + dayOffset); + return d; + }, [dayOffset]); + + // 모달이 열릴 때마다 초기화 + useEffect(() => { + if (lot) { + setDayOffset(1); + setPlate(''); + setErr(null); + setBusy(false); + } + }, [lot]); + + async function submit() { + if (!lot) return; + setBusy(true); + setErr(null); + try { + await purchaseParkingPass({ + lotId: lot.lotId, + useDate: ymd(useDate), + eventId, + vehiclePlate: plate.trim() ? plate.trim() : undefined, + payMethod: 'card', + }); + onPurchased(); + } catch (e) { + setErr((e as Error).message || t('parking.purchaseErr')); + setBusy(false); + } + } + + return ( + + + + + {t('parking.purchaseTitle')} + + + + + + {lot ? ( + <> + {/* 선택 주차장 */} + {t('parking.selectLot')} + + {lot.name} + {t('parking.passPrice', { n: won(lot.passPrice) })} + + + {/* 이용일 스텝퍼 */} + {t('parking.useDate')} + + setDayOffset((v) => Math.max(1, v - 1))} + accessibilityLabel={t('parking.prevDay')} + > + + + {ymd(useDate)} + = 14} + onPress={() => setDayOffset((v) => Math.min(14, v + 1))} + accessibilityLabel={t('parking.nextDay')} + > + = 14 ? colors.neutral200 : colors.neutral700} + /> + + + + {/* 차량번호(선택) */} + {t('parking.plate')} + + {t('parking.plateNote')} + {t('parking.payNote')} + + {err ? {err} : null} + + + {busy ? ( + + ) : ( + {t('parking.confirm')} + )} + + + ) : null} + + + + ); +} + +const styles = StyleSheet.create({ + flex: { flex: 1, backgroundColor: colors.neutral050 }, + scroll: { padding: spacing.md, gap: spacing.sm, paddingBottom: 48 }, + loading: { paddingVertical: spacing.xl, alignItems: 'center' }, + segment: { + flexDirection: 'row', + margin: spacing.md, + marginBottom: 0, + 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 }, + // 혼잡 + overallCard: { + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.neutral200, + borderRadius: radius.md, + padding: spacing.md, + gap: 10, + }, + overallTop: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, + overallLabel: { fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 }, + overallMetric: { flexDirection: 'row', alignItems: 'center', gap: 6 }, + overallCount: { fontSize: type.body.fontSize, color: colors.neutral700, fontWeight: '600' }, + group: { + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.neutral200, + borderRadius: radius.md, + padding: spacing.md, + gap: 8, + }, + groupHeader: { flexDirection: 'row', alignItems: 'center', gap: 6 }, + groupTitle: { fontSize: type.body.fontSize, fontWeight: '700', color: colors.neutral900 }, + areaRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: 10, + borderTopWidth: 1, + borderTopColor: colors.neutral200, + paddingTop: 8, + }, + areaLabel: { flex: 1, fontSize: type.body.fontSize, color: colors.neutral700 }, + // 주차 lot + lotCard: { + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.neutral200, + borderRadius: radius.md, + padding: spacing.md, + gap: 8, + }, + lotTop: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }, + lotName: { flex: 1, fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 }, + bar: { + height: 8, + borderRadius: 4, + backgroundColor: colors.neutral200, + overflow: 'hidden', + }, + barFill: { height: 8, borderRadius: 4, backgroundColor: colors.primary600 }, + lotMetaRow: { flexDirection: 'row' }, + lotMeta: { fontSize: type.caption.fontSize, color: colors.neutral500 }, + buyBtn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 6, + minHeight: touch.min, + borderRadius: radius.sm, + backgroundColor: colors.primary600, + marginTop: 2, + }, + buyBtnGhost: { backgroundColor: colors.neutral050, borderWidth: 1, borderColor: colors.neutral200 }, + buyText: { color: colors.white, fontSize: type.body.fontSize, fontWeight: '700' }, + buyHint: { fontSize: 11, color: colors.neutral500, textAlign: 'center' }, + // 주차권 + loginNote: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: colors.primary050, + borderRadius: radius.md, + paddingHorizontal: 14, + paddingVertical: 12, + }, + loginNoteText: { fontSize: type.body.fontSize, color: colors.primary700, fontWeight: '600' }, + emptyNote: { fontSize: type.caption.fontSize, color: colors.neutral500, paddingVertical: spacing.sm }, + passCard: { + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.neutral200, + borderRadius: radius.md, + padding: spacing.md, + gap: 6, + }, + passDim: { opacity: 0.6 }, + passTop: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }, + passLot: { flex: 1, fontSize: type.body.fontSize, fontWeight: '700', color: colors.neutral900 }, + passBadge: { borderRadius: radius.pill, paddingHorizontal: 10, paddingVertical: 3 }, + passBadgeText: { fontSize: 11, fontWeight: '700' }, + passMeta: { fontSize: type.caption.fontSize, color: colors.neutral500 }, + passBottom: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, + passAmount: { fontSize: type.body.fontSize, fontWeight: '700', color: colors.primary700 }, + // 모달 + modalBackdrop: { flex: 1, backgroundColor: 'rgba(16,24,40,0.45)', justifyContent: 'flex-end' }, + modalSheet: { + backgroundColor: colors.white, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + padding: spacing.md, + paddingBottom: spacing.xl, + gap: 8, + }, + modalHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, + modalTitle: { fontSize: type.h2.fontSize, fontWeight: '700', color: colors.neutral900 }, + modalLabel: { fontSize: type.caption.fontSize, color: colors.neutral500, fontWeight: '600', marginTop: 6 }, + modalLotBox: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: colors.neutral050, + borderRadius: radius.sm, + padding: 12, + }, + modalLotName: { flex: 1, fontSize: type.body.fontSize, fontWeight: '600', color: colors.neutral900 }, + modalLotPrice: { fontSize: type.body.fontSize, fontWeight: '700', color: colors.primary700 }, + dateRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: colors.neutral050, + borderRadius: radius.sm, + borderWidth: 1, + borderColor: colors.neutral200, + }, + dateBtn: { width: touch.min, height: touch.min, alignItems: 'center', justifyContent: 'center' }, + dateText: { fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 }, + input: { + minHeight: touch.min, + borderWidth: 1, + borderColor: colors.neutral200, + borderRadius: radius.sm, + paddingHorizontal: 12, + fontSize: type.body.fontSize, + color: colors.neutral900, + backgroundColor: colors.white, + }, + modalNote: { fontSize: 11, color: colors.neutral500, lineHeight: 16 }, + confirmBtn: { + minHeight: 52, + borderRadius: radius.md, + backgroundColor: colors.primary600, + alignItems: 'center', + justifyContent: 'center', + marginTop: 8, + }, + confirmBusy: { opacity: 0.7 }, + confirmText: { color: colors.white, fontSize: type.h3.fontSize, fontWeight: '700' }, +}); diff --git a/mobile/app/(visitor)/tickets.tsx b/mobile/app/(visitor)/tickets.tsx new file mode 100644 index 0000000..1601698 --- /dev/null +++ b/mobile/app/(visitor)/tickets.tsx @@ -0,0 +1,10 @@ +/* + * SCR-M15 [모바일] 관람객(B2C) 티켓 탭 — 공용 TicketWallet 본문 재사용. + * 헤더는 탭 레이아웃이 소유. 기존 /tickets 라우트와 동일 본문(편입). + */ +import React from 'react'; +import { TicketWallet } from '../../components/tickets/TicketWallet'; + +export default function VisitorTicketsScreen() { + return ; +} diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index 3f0c77b..a9d1414 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -30,6 +30,7 @@ function RootStack() { + diff --git a/mobile/app/login.tsx b/mobile/app/login.tsx index 1e6cbe5..1f98802 100644 --- a/mobile/app/login.tsx +++ b/mobile/app/login.tsx @@ -7,6 +7,7 @@ import { Link, router } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { + Image, KeyboardAvoidingView, Platform, Pressable, @@ -104,9 +105,15 @@ export default function LoginScreen() { contentContainerStyle={[styles.scroll, { paddingTop: insets.top + spacing.lg }]} keyboardShouldPersistTaps="handled" > - {/* 브랜드 패널 */} + {/* 브랜드 패널 — 정식 CI 워드마크(CI_01, 딥블루 패널용 화이트 변형) */} - KINTEX + {t('login.brandTitle')} {t('login.brandCaption')} @@ -193,7 +200,7 @@ const styles = StyleSheet.create({ padding: spacing.lg, gap: 8, }, - brandMark: { color: colors.white, fontSize: 22, fontWeight: '800', letterSpacing: 1 }, + brandMark: { width: 178, height: 32, marginBottom: 2 }, brandTitle: { color: colors.white, fontSize: type.h2.fontSize, fontWeight: '700', lineHeight: 30 }, brandCaption: { color: 'rgba(255,255,255,0.75)', fontSize: type.caption.fontSize }, card: { diff --git a/mobile/app/tickets/index.tsx b/mobile/app/tickets/index.tsx index 852e3b4..9797803 100644 --- a/mobile/app/tickets/index.tsx +++ b/mobile/app/tickets/index.tsx @@ -1,191 +1,18 @@ /* - * SCR-M15 [모바일] 내 티켓 지갑 — 관람객(B2C) 트랙. - * 필터 탭(진행중·예정·지난) + 티켓 카드 리스트 + QR 풀스크린 뷰어. - * 배지 전환 안내 배너 + 오프라인 표시 배지. 사용됨/취소 카드 흐리게. - * ※ M10(티켓)·M9(환불) 백엔드 미구현 → 샘플 데이터("샘플" 배지). API 호출 없음. + * SCR-M15 [모바일] 내 티켓 지갑 — 레거시 스택 라우트(/tickets). + * 본문은 공용 TicketWallet 컴포넌트(관람객 탭 (visitor)/tickets와 동일). 딥링크·예매 완료 복귀 호환용. */ -import { Ionicons } from '@expo/vector-icons'; -import { router, Stack } from 'expo-router'; -import React, { useMemo, useState } from 'react'; +import { Stack } from 'expo-router'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; -import { Banner } from '../../components/Banner'; -import { useSecureScreen } from '../../context/SecureScreenContext'; -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'; +import { TicketWallet } from '../../components/tickets/TicketWallet'; export default function TicketWalletScreen() { const { t } = useTranslation(); - // 티켓 QR 화면 — 캡처 차단(QR 재사용 방지) + 백그라운드 마스킹(B4/B7). - useSecureScreen('tickets'); - - 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(ticket: SampleTicket) { - // SCR-P8(예매 확인·취소) 미구현 → 안내만(샘플) - Alert.alert( - t('tickets.detailTitle'), - `${ticket.eventName}\n${ticket.bookingNoMasked}`, - ); - } - return ( - + <> - - - {/* 오프라인 표시 배지 */} - - - - {t('tickets.offline')} - - - - {/* 필터 탭(세그먼트) */} - - {FILTER_TABS.map((tab) => { - const on = filter === tab.key; - return ( - setFilter(tab.key)} - > - {tab.label} - - ); - })} - - - {/* 배지 전환 안내 배너 */} - {t('tickets.badgeNotice')} - - {/* 티켓 리스트 / 빈 상태 */} - {filtered.length === 0 ? ( - - ) : ( - filtered.map((tk) => ( - - )) - )} - - {t('tickets.footNote')} - - - setQrOpen(false)} - /> - + + ); } - -function EmptyState() { - const { t } = useTranslation(); - return ( - - - {t('tickets.emptyTitle')} - {t('tickets.emptyBody')} - router.push('/tickets/select')} - > - {t('tickets.emptyBtn')} - - - ); -} - -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' }, -}); diff --git a/mobile/assets/logo-wordmark-white.png b/mobile/assets/logo-wordmark-white.png new file mode 100644 index 0000000000000000000000000000000000000000..1c28c176842a223f3958c5fc73b5d073f6d50d1a GIT binary patch literal 4335 zcmVc6<+h7D#DYN7BoUDPbPpyL@rMXoR$YHUSgDszIZ`ijDU%;t>A;P1@!^p5xm6^ zKztFbm{3~!Lc&cATHukZ3G}pt7bR%VjM-(a_Ur1JnLV@DKmVNb?=P9mf0@~P&z`;4 zUiV#8RaFpCI4V9g!heVguAderMtiVM`Cr;ohoYvv^2}wAw)J`T3DhP1iDjHaM877Y z3meM#6%lPEqJu^ouzYM|m$o+b36@v)d((YQkayYleKb9PAL#jj?@Q{Db6q`s`aczV zev}c{c5R<^y~q2La>@?hqY8YIh?e=r$~a1ORB*nP(ov{EMNs}zsvYuK+8d64^F0gv zJQFSlJ6+ciet*2?dE1Uq=VHe`){u#^j{ARq3tz3R&7L0w7J(q6^lLA8=I(E9e;!_k`8tG1ZwkAmzK}=2D;gy&bNV_Mj*o z+OjLL4NJ^5$m&aKwbUzY8!U0zg610W{z@%0D-H4t$UzW%$Au|;-a@m-9$x5>Kuipx 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%0DKXf=t8 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 ye|O-&JMiBf`0oz>FVu2rbti}*hSLlpS{ literal 31218 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 diff --git a/mobile/components/CongestionPill.tsx b/mobile/components/CongestionPill.tsx new file mode 100644 index 0000000..f8efdf0 --- /dev/null +++ b/mobile/components/CongestionPill.tsx @@ -0,0 +1,51 @@ +/* + * 혼잡도 필 — 여유(FREE)/보통(NORMAL)/혼잡(BUSY) 3단계. + * 색 + 텍스트 병기(WCAG: 색만으로 정보 전달 금지). 점유율(%) 선택 노출. + */ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import type { CongestionLevel } from '../lib/types'; +import { colors, radius, type } from '../theme'; + +const LEVEL_STYLE: Record = { + FREE: { color: colors.success, bg: '#E7F6EF' }, + NORMAL: { color: colors.warning, bg: '#FFF7ED' }, + BUSY: { color: colors.error, bg: '#FEF3F2' }, +}; + +export function CongestionPill({ + level, + label, + percent, + size = 'md', +}: { + level: CongestionLevel; + label: string; + percent?: number | null; + size?: 'sm' | 'md'; +}) { + const s = LEVEL_STYLE[level] ?? LEVEL_STYLE.NORMAL; + const text = percent != null ? `${label} ${percent}%` : label; + return ( + + + {text} + + ); +} + +const styles = StyleSheet.create({ + pill: { + flexDirection: 'row', + alignItems: 'center', + gap: 5, + borderRadius: radius.pill, + paddingHorizontal: 10, + paddingVertical: 4, + alignSelf: 'flex-start', + }, + pillSm: { paddingHorizontal: 8, paddingVertical: 2 }, + dot: { width: 7, height: 7, borderRadius: 4 }, + text: { fontSize: type.caption.fontSize, fontWeight: '700' }, + textSm: { fontSize: 11 }, +}); diff --git a/mobile/components/LiveNoticeFeed.tsx b/mobile/components/LiveNoticeFeed.tsx new file mode 100644 index 0000000..36483d3 --- /dev/null +++ b/mobile/components/LiveNoticeFeed.tsx @@ -0,0 +1,173 @@ +/* + * 라이브 공지 피드 — 공개 GET /api/public/events/{id}/live-notices. + * 고정(pinned) 상단 우선 → 최신순(서버 정렬 신뢰). 긴급(URGENT) 강조. 30초 폴링. + * 상태: 로딩·빈·degraded(마지막 데이터 유지 + 안내). 홈/행사 상세 공용. + * 근거: src/backend/_workspace/cms_backlog_contract.md (공개 라이브 공지). + */ +import { Ionicons } from '@expo/vector-icons'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'; +import { isDegraded } from '../lib/api'; +import { getLiveNotices } from '../lib/visitor'; +import type { LiveNoticeCategory, LiveNoticeDto } from '../lib/types'; +import { colors, radius, spacing, type } from '../theme'; + +const POLL_MS = 30_000; + +/** 카테고리 → 배지 색(계약 권장 매핑). */ +const CATEGORY_STYLE: Record = { + URGENT: { color: colors.error, bg: '#FEF3F2' }, + PROGRAM: { color: colors.warning, bg: '#FFF7ED' }, + INFO: { color: colors.primary700, bg: colors.primary050 }, + GENERAL: { color: colors.neutral700, bg: colors.neutral050 }, +}; + +interface Props { + eventId: string; + /** 노출 최대 건수(홈=3 요약, 행사=전체). */ + max?: number; +} + +export function LiveNoticeFeed({ eventId, max }: Props) { + const { t } = useTranslation(); + const [items, setItems] = useState(null); + const [degraded, setDegraded] = useState(false); + const mounted = useRef(true); + + const catLabel = useCallback( + (c: LiveNoticeCategory): string => t(`notices.cat.${c}` as const), + [t], + ); + + const load = useCallback(async () => { + try { + const res = await getLiveNotices(eventId, 100); + if (!mounted.current) return; + // 서버가 pinned 우선→최신 정렬. 방어적으로 pinned 재정렬(안정 정렬). + const sorted = [...(res ?? [])].sort((a, b) => Number(b.pinned) - Number(a.pinned)); + setItems(sorted); + setDegraded(false); + } catch (e) { + if (!mounted.current) return; + setDegraded(isDegraded(e)); + setItems((prev) => prev ?? []); // 최초 실패는 빈 배열로(무한 로딩 방지) + } + }, [eventId]); + + useEffect(() => { + mounted.current = true; + load(); + const timer = setInterval(load, POLL_MS); + return () => { + mounted.current = false; + clearInterval(timer); + }; + }, [load]); + + const visible = max != null ? (items ?? []).slice(0, max) : items ?? []; + + return ( + + + + + {t('notices.title')} + + + + {t('notices.live')} + + + + {items == null ? ( + + + + ) : visible.length === 0 ? ( + + {degraded ? t('notices.degraded') : t('notices.empty')} + + ) : ( + + {degraded ? {t('notices.degraded')} : null} + {visible.map((n) => ( + + ))} + + )} + + ); +} + +function NoticeRow({ + notice, + catLabel, +}: { + notice: LiveNoticeDto; + catLabel: (c: LiveNoticeCategory) => string; +}) { + const { t } = useTranslation(); + const urgent = notice.category === 'URGENT'; + const cat = CATEGORY_STYLE[notice.category] ?? CATEGORY_STYLE.GENERAL; + return ( + + + + {catLabel(notice.category)} + + {notice.pinned ? ( + + + {t('notices.pinned')} + + ) : null} + + + {notice.title} + + {notice.body ? ( + + {notice.body} + + ) : null} + {notice.authorName ? {notice.authorName} : null} + + ); +} + +const styles = StyleSheet.create({ + wrap: { + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.neutral200, + borderRadius: radius.md, + padding: spacing.md, + gap: spacing.sm, + }, + header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, + headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 6 }, + title: { fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 }, + liveDot: { flexDirection: 'row', alignItems: 'center', gap: 5 }, + liveDotInner: { width: 6, height: 6, borderRadius: 3, backgroundColor: colors.error }, + liveText: { fontSize: 11, fontWeight: '700', color: colors.error }, + loading: { paddingVertical: spacing.md, alignItems: 'center' }, + empty: { fontSize: type.caption.fontSize, color: colors.neutral500, paddingVertical: spacing.sm }, + degradedNote: { fontSize: 11, color: colors.neutral500 }, + list: { gap: spacing.sm }, + row: { + borderTopWidth: 1, + borderTopColor: colors.neutral200, + paddingTop: spacing.sm, + gap: 4, + }, + rowUrgent: { borderLeftWidth: 3, borderLeftColor: colors.error, paddingLeft: 8 }, + rowTop: { flexDirection: 'row', alignItems: 'center', gap: 6 }, + catBadge: { borderRadius: radius.sm, paddingHorizontal: 6, paddingVertical: 2 }, + catText: { fontSize: 10, fontWeight: '700' }, + pinBadge: { flexDirection: 'row', alignItems: 'center', gap: 3 }, + pinText: { fontSize: 10, fontWeight: '600', color: colors.primary700 }, + rowTitle: { fontSize: type.body.fontSize, fontWeight: '600', color: colors.neutral900 }, + rowBody: { fontSize: type.caption.fontSize, color: colors.neutral700, lineHeight: 18 }, + rowMeta: { fontSize: 11, color: colors.neutral500 }, +}); diff --git a/mobile/components/tickets/TicketWallet.tsx b/mobile/components/tickets/TicketWallet.tsx new file mode 100644 index 0000000..bae7ffc --- /dev/null +++ b/mobile/components/tickets/TicketWallet.tsx @@ -0,0 +1,177 @@ +/* + * SCR-M15 내 티켓 지갑 (본문 컴포넌트) — 관람객(B2C) 트랙. + * 필터 탭(진행중·예정·지난) + 티켓 카드 + QR 풀스크린 뷰어 + 배지 전환 안내 + 오프라인 배지. + * 헤더(Stack/Tab)는 호출 측이 소유 — 이 컴포넌트는 본문만 렌더(탭·스택 양쪽 재사용). + * ※ M10(티켓)·M9(환불) 백엔드 미구현 → 샘플 데이터. API 호출 없음. + */ +import { Ionicons } from '@expo/vector-icons'; +import { router } from 'expo-router'; +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { useSecureScreen } from '../../context/SecureScreenContext'; +import { colors, radius, spacing, type } from '../../theme'; +import { Banner } from '../Banner'; +import { QrViewerModal } from './QrViewerModal'; +import { TicketCard } from './TicketCard'; +import { + FILTER_TABS, + SAMPLE_TICKETS, + type SampleTicket, + type TicketFilter, +} from './sampleTickets'; + +export function TicketWallet() { + const { t } = useTranslation(); + // 티켓 QR 화면 — 캡처 차단(QR 재사용 방지) + 백그라운드 마스킹(B4/B7). + useSecureScreen('tickets'); + + const [filter, setFilter] = useState('active'); + const [qrOpen, setQrOpen] = useState(false); + const [qrIndex, setQrIndex] = useState(0); + + const filtered = useMemo(() => SAMPLE_TICKETS.filter((tk) => tk.filter === filter), [filter]); + const usableInView = useMemo(() => filtered.filter((tk) => tk.status === 'usable'), [filtered]); + + function openQr(tk: SampleTicket) { + const i = usableInView.findIndex((x) => x.id === tk.id); + setQrIndex(i < 0 ? 0 : i); + setQrOpen(true); + } + + function openDetail(ticket: SampleTicket) { + Alert.alert(t('tickets.detailTitle'), `${ticket.eventName}\n${ticket.bookingNoMasked}`); + } + + return ( + + + {/* 오프라인 표시 배지 */} + + + + {t('tickets.offline')} + + + + {/* 필터 탭(세그먼트) */} + + {FILTER_TABS.map((tab) => { + const on = filter === tab.key; + return ( + setFilter(tab.key)} + > + {tab.label} + + ); + })} + + + {/* 배지 전환 안내 배너 */} + {t('tickets.badgeNotice')} + + {/* 티켓 리스트 / 빈 상태 */} + {filtered.length === 0 ? ( + + ) : ( + filtered.map((tk) => ( + + )) + )} + + {t('tickets.footNote')} + + + setQrOpen(false)} + /> + + ); +} + +function EmptyState() { + const { t } = useTranslation(); + return ( + + + {t('tickets.emptyTitle')} + {t('tickets.emptyBody')} + router.push('/tickets/select')} + > + {t('tickets.emptyBtn')} + + + ); +} + +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' }, +}); diff --git a/mobile/lib/config.ts b/mobile/lib/config.ts index d6f0f20..f71a182 100644 --- a/mobile/lib/config.ts +++ b/mobile/lib/config.ts @@ -13,3 +13,10 @@ export const API_BASE: string = /** AI 생성 이미지 기본 고지문 (백엔드 watermarkText 부재 시 폴백 — 계약 §0-3). */ export const AI_IMAGE_NOTICE = 'AI 생성 예상 이미지 — 실제 시공 결과와 다를 수 있습니다'; + +/** + * 공개(B2C) 조회 폴백 행사 ID — 로그인 관람객이 참여 워크스페이스가 없거나 + * 게스트로 공개 혼잡/주차/공지를 조회할 때 사용하는 데모 라이브 행사. + * 시크릿 아님(공개 식별자). 활성 워크스페이스가 있으면 그것을 우선한다. + */ +export const DEFAULT_PUBLIC_EVENT_ID = 'e-2026-live'; diff --git a/mobile/lib/i18n/locales/en.ts b/mobile/lib/i18n/locales/en.ts index 7b612a1..a586851 100644 --- a/mobile/lib/i18n/locales/en.ts +++ b/mobile/lib/i18n/locales/en.ts @@ -201,4 +201,91 @@ export const en: Translations = { emptyBtn: 'Book entry pass', detailTitle: 'Booking detail · cancel', }, + vnav: { + tabHome: 'Home', + tabEvents: 'Events', + tabTickets: 'Tickets', + tabOnsite: 'On-site', + tabMy: 'My', + }, + vhome: { + hello: 'Welcome, {{name}}', + subtitle: "Explore today's exhibition with ease", + noEvent: 'You are not part of any event — book a pass and it will appear here.', + tileTickets: 'My Tickets', + tileTicketsDesc: 'Entry QR · badge', + tileEvents: 'Events', + tileEventsDesc: 'Notices · schedule', + tileOnsite: 'On-site', + tileOnsiteDesc: 'Crowd · parking', + aiTitle: 'AI Visit Assistant', + aiDesc: 'Live crowd, parking and notices. Booth search and wayfinding coming soon.', + }, + vevents: { + title: 'Events', + empty: 'You are not part of any event — showing public notices.', + }, + notices: { + title: 'Live Notices', + live: 'LIVE', + pinned: 'Pinned', + empty: 'No notices posted.', + degraded: 'Could not load notices (check connection).', + cat: { + URGENT: 'Urgent', + PROGRAM: 'Program', + GENERAL: 'General', + INFO: 'Info', + }, + }, + onsite: { + segCongestion: 'Crowd', + segParking: 'Parking', + }, + congestion: { + overall: 'Overall crowd', + onSite: 'On-site', + people: '{{n}} people', + gates: 'Entry gates', + parking: 'Parking', + popular: 'Popular areas', + empty: 'No crowd data to show.', + degraded: 'Could not load crowd data (check connection).', + }, + parking: { + title: 'Parking status', + empty: 'No parking data.', + degraded: 'Could not load parking data (check connection).', + available: '{{n}} free', + capacity: '{{n}} total', + rate: '{{n}} KRW/hr', + daily: 'daily max {{n}} KRW', + passPrice: '{{n}} KRW', + buyPass: 'Buy pass', + myPasses: 'My passes', + noPasses: 'You have no parking passes.', + loginRequired: 'Sign in to buy or view parking passes.', + purchaseTitle: 'Buy parking pass', + selectLot: 'Parking lot', + useDate: 'Use date', + plate: 'Vehicle plate (optional)', + platePlaceholder: 'e.g. 123가4567', + plateNote: 'The plate is masked and not stored.', + payNote: 'Payment is simulated (mock) — no real charge.', + confirm: 'Buy', + purchaseErr: 'Failed to buy the parking pass.', + prevDay: 'Previous day', + nextDay: 'Next day', + passStatus: { + PAID: 'Paid', + USED: 'Used', + CANCELLED: 'Cancelled', + }, + }, + vmy: { + tickets: 'My Tickets', + ticketsDesc: 'Entry QR · badge switch', + parking: 'My Passes', + parkingDesc: 'Parking status · pre-pass', + }, }; diff --git a/mobile/lib/i18n/locales/ja.ts b/mobile/lib/i18n/locales/ja.ts index e80dac2..dde4bc9 100644 --- a/mobile/lib/i18n/locales/ja.ts +++ b/mobile/lib/i18n/locales/ja.ts @@ -201,4 +201,91 @@ export const ja: Translations = { emptyBtn: '入場券を予約', detailTitle: '予約詳細 · キャンセル', }, + vnav: { + tabHome: 'ホーム', + tabEvents: 'イベント', + tabTickets: 'チケット', + tabOnsite: '現場', + tabMy: 'マイ', + }, + vhome: { + hello: '{{name}}さん、ようこそ', + subtitle: '今日の展示会を気軽に見て回りましょう', + noEvent: '参加中のイベントがありません — 入場券を予約するとここに表示されます。', + tileTickets: 'マイチケット', + tileTicketsDesc: '入場QR · バッジ', + tileEvents: 'イベント', + tileEventsDesc: 'お知らせ · 日程', + tileOnsite: '現場', + tileOnsiteDesc: '混雑 · 駐車', + aiTitle: 'AI 観覧アシスタント', + aiDesc: '混雑・駐車・お知らせをリアルタイムに案内します。ブース検索・道案内も近日追加。', + }, + vevents: { + title: 'イベント', + empty: '参加中のイベントがありません — 公開のお知らせを表示します。', + }, + notices: { + title: 'ライブお知らせ', + live: 'LIVE', + pinned: '固定', + empty: '登録されたお知らせがありません。', + degraded: 'お知らせを取得できませんでした(接続を確認)。', + cat: { + URGENT: '緊急', + PROGRAM: 'プログラム', + GENERAL: '一般', + INFO: '案内', + }, + }, + onsite: { + segCongestion: '混雑度', + segParking: '駐車', + }, + congestion: { + overall: '総合混雑度', + onSite: '現場人数', + people: '{{n}}人', + gates: '入場ゲート', + parking: '駐車場', + popular: '人気エリア', + empty: '表示できる混雑情報がありません。', + degraded: '混雑情報を取得できませんでした(接続を確認)。', + }, + parking: { + title: '駐車状況', + empty: '駐車場情報がありません。', + degraded: '駐車情報を取得できませんでした(接続を確認)。', + available: '空き {{n}}台', + capacity: '全 {{n}}台', + rate: '1時間 {{n}}ウォン', + daily: '1日上限 {{n}}ウォン', + passPrice: '{{n}}ウォン', + buyPass: '駐車券購入', + myPasses: 'マイ駐車券', + noPasses: '保有する駐車券がありません。', + loginRequired: '駐車券の購入・照会にはログインが必要です。', + purchaseTitle: '事前駐車券の購入', + selectLot: '駐車場', + useDate: '利用日', + plate: '車両番号(任意)', + platePlaceholder: '例:123가4567', + plateNote: '車両番号はマスキングされ保存されません。', + payNote: '決済はシミュレーション(mock)です — 実際に請求されません。', + confirm: '購入', + purchaseErr: '駐車券の購入に失敗しました。', + prevDay: '前の日', + nextDay: '次の日', + passStatus: { + PAID: '決済完了', + USED: '使用済み', + CANCELLED: 'キャンセル', + }, + }, + vmy: { + tickets: 'マイチケット', + ticketsDesc: '入場QR · バッジ切替', + parking: 'マイ駐車券', + parkingDesc: '駐車状況 · 事前駐車券', + }, }; diff --git a/mobile/lib/i18n/locales/ko.ts b/mobile/lib/i18n/locales/ko.ts index 59a45df..635d21d 100644 --- a/mobile/lib/i18n/locales/ko.ts +++ b/mobile/lib/i18n/locales/ko.ts @@ -203,6 +203,93 @@ export const ko = { emptyBtn: '입장권 예매', detailTitle: '예매 상세·취소', }, + vnav: { + tabHome: '홈', + tabEvents: '행사', + tabTickets: '티켓', + tabOnsite: '현장', + tabMy: '마이', + }, + vhome: { + hello: '{{name}}님, 환영합니다', + subtitle: '오늘의 전시를 편하게 둘러보세요', + noEvent: '참여 중인 행사가 없습니다 — 입장권을 예매하면 여기에 표시됩니다.', + tileTickets: '내 티켓', + tileTicketsDesc: '입장 QR · 배지', + tileEvents: '행사', + tileEventsDesc: '공지 · 일정', + tileOnsite: '현장', + tileOnsiteDesc: '혼잡 · 주차', + aiTitle: 'AI 관람 도우미', + aiDesc: '혼잡·주차·공지를 실시간으로 안내합니다. 곧 부스 검색·길찾기도 추가됩니다.', + }, + vevents: { + title: '행사', + empty: '참여 중인 행사가 없습니다 — 공개 공지를 표시합니다.', + }, + notices: { + title: '라이브 공지', + live: 'LIVE', + pinned: '고정', + empty: '등록된 공지가 없습니다.', + degraded: '공지를 불러오지 못했습니다 (연결 확인).', + cat: { + URGENT: '긴급', + PROGRAM: '프로그램', + GENERAL: '일반', + INFO: '안내', + }, + }, + onsite: { + segCongestion: '혼잡도', + segParking: '주차', + }, + congestion: { + overall: '종합 혼잡도', + onSite: '현장 인원', + people: '{{n}}명', + gates: '입장 게이트', + parking: '주차장', + popular: '인기 공간', + empty: '표시할 혼잡 정보가 없습니다.', + degraded: '혼잡 정보를 불러오지 못했습니다 (연결 확인).', + }, + parking: { + title: '주차 현황', + empty: '주차장 정보가 없습니다.', + degraded: '주차 정보를 불러오지 못했습니다 (연결 확인).', + available: '여유 {{n}}면', + capacity: '전체 {{n}}면', + rate: '시간당 {{n}}원', + daily: '일 최대 {{n}}원', + passPrice: '{{n}}원', + buyPass: '주차권 구매', + myPasses: '내 주차권', + noPasses: '보유한 주차권이 없습니다.', + loginRequired: '주차권 구매·조회는 로그인이 필요합니다.', + purchaseTitle: '사전 주차권 구매', + selectLot: '주차장', + useDate: '이용일', + plate: '차량번호 (선택)', + platePlaceholder: '예: 123가4567', + plateNote: '차량번호는 마스킹되어 저장되지 않습니다.', + payNote: '결제는 시뮬레이션(mock)입니다 — 실제 청구되지 않습니다.', + confirm: '구매', + purchaseErr: '주차권 구매에 실패했습니다.', + prevDay: '이전 날짜', + nextDay: '다음 날짜', + passStatus: { + PAID: '결제완료', + USED: '사용됨', + CANCELLED: '취소', + }, + }, + vmy: { + tickets: '내 티켓', + ticketsDesc: '입장 QR · 배지 전환', + parking: '내 주차권', + parkingDesc: '주차 현황 · 사전 주차권', + }, } as const; export type Resource = typeof ko; diff --git a/mobile/lib/i18n/locales/zh.ts b/mobile/lib/i18n/locales/zh.ts index 3e8b9eb..fd134ae 100644 --- a/mobile/lib/i18n/locales/zh.ts +++ b/mobile/lib/i18n/locales/zh.ts @@ -200,4 +200,91 @@ export const zh: Translations = { emptyBtn: '预订入场券', detailTitle: '预订详情 · 取消', }, + vnav: { + tabHome: '首页', + tabEvents: '展会', + tabTickets: '门票', + tabOnsite: '现场', + tabMy: '我的', + }, + vhome: { + hello: '{{name}},欢迎', + subtitle: '轻松逛今天的展会', + noEvent: '您尚未参加任何展会 — 预订门票后将显示在此处。', + tileTickets: '我的门票', + tileTicketsDesc: '入场二维码 · 徽章', + tileEvents: '展会', + tileEventsDesc: '公告 · 日程', + tileOnsite: '现场', + tileOnsiteDesc: '拥挤 · 停车', + aiTitle: 'AI 观展助手', + aiDesc: '实时提供拥挤、停车与公告信息。展位搜索与导航即将上线。', + }, + vevents: { + title: '展会', + empty: '您尚未参加任何展会 — 显示公开公告。', + }, + notices: { + title: '实时公告', + live: 'LIVE', + pinned: '置顶', + empty: '暂无公告。', + degraded: '无法加载公告(请检查网络)。', + cat: { + URGENT: '紧急', + PROGRAM: '节目', + GENERAL: '一般', + INFO: '提示', + }, + }, + onsite: { + segCongestion: '拥挤度', + segParking: '停车', + }, + congestion: { + overall: '综合拥挤度', + onSite: '现场人数', + people: '{{n}}人', + gates: '入场闸口', + parking: '停车场', + popular: '热门区域', + empty: '暂无拥挤信息。', + degraded: '无法加载拥挤信息(请检查网络)。', + }, + parking: { + title: '停车现况', + empty: '暂无停车场信息。', + degraded: '无法加载停车信息(请检查网络)。', + available: '空位 {{n}}', + capacity: '共 {{n}}', + rate: '每小时 {{n}} 韩元', + daily: '每日最高 {{n}} 韩元', + passPrice: '{{n}} 韩元', + buyPass: '购买停车券', + myPasses: '我的停车券', + noPasses: '暂无停车券。', + loginRequired: '购买或查看停车券需要登录。', + purchaseTitle: '购买预约停车券', + selectLot: '停车场', + useDate: '使用日期', + plate: '车牌号(可选)', + platePlaceholder: '例如:123가4567', + plateNote: '车牌号将被掩码且不予保存。', + payNote: '支付为模拟(mock)— 不会实际收费。', + confirm: '购买', + purchaseErr: '停车券购买失败。', + prevDay: '前一天', + nextDay: '后一天', + passStatus: { + PAID: '已支付', + USED: '已使用', + CANCELLED: '已取消', + }, + }, + vmy: { + tickets: '我的门票', + ticketsDesc: '入场二维码 · 徽章转换', + parking: '我的停车券', + parkingDesc: '停车现况 · 预约停车券', + }, }; diff --git a/mobile/lib/roleTrack.ts b/mobile/lib/roleTrack.ts index 0ac610f..89203eb 100644 --- a/mobile/lib/roleTrack.ts +++ b/mobile/lib/roleTrack.ts @@ -52,14 +52,14 @@ export function resolveLandingTrack({ user, workspaces }: LandingInput): Track { * 트랙 → 로그인 직후 랜딩 라우트(모바일). * · admin/ops/business → 탭 홈(/(tabs)) — 모바일엔 별도 admin 화면 없음, 홈이 역할 진입 허브 * · agency(장치·시공) → 현장 탭(/(tabs)/field) — 체크리스트 허브 - * · visitor(관람객) → 티켓 지갑(/tickets) — B2C 진입 + * · visitor(관람객) → 관람객(B2C) 탭 셸(/(visitor)) — 홈·행사·티켓·현장·마이 */ export function landingPathForTrack(track: Track): string { switch (track) { case 'agency': return '/(tabs)/field'; case 'visitor': - return '/tickets'; + return '/(visitor)'; case 'admin': case 'ops': case 'business': diff --git a/mobile/lib/types.ts b/mobile/lib/types.ts index 9e2d1bb..9baeb98 100644 --- a/mobile/lib/types.ts +++ b/mobile/lib/types.ts @@ -129,3 +129,94 @@ export interface OtpStatusDto { export interface AvatarUploadResponse { photoUrl: string; } + +// ── B2C 관람객: 혼잡·주차 (parking_congestion_contract.md) ── +/** 혼잡/점유 수준. 여유/보통/혼잡 3단계. */ +export type CongestionLevel = 'FREE' | 'NORMAL' | 'BUSY'; + +/** 주차장 현황 — GET /api/public/parking/lots (공개). */ +export interface ParkingLotStatusDto { + lotId: string; + code: string; + name: string; + exhibitionCenter: number | null; // 1|2|null(공용) + totalCapacity: number; + occupied: number; + available: number; + occupancyPercent: number; + congestionLevel: CongestionLevel; + congestionLabel: string; // 여유|보통|혼잡 + hourlyRate: number; + dailyMax: number | null; + passPrice: number; // 사전 주차권(1일권) 가격 + note?: string | null; + updatedAt: string; +} + +/** 사전 주차권 구매 요청 — POST /api/parking/passes (인증). */ +export interface PurchasePassRequest { + lotId: string; + useDate: string; // YYYY-MM-DD, 오늘 이후 + eventId?: string; + vehiclePlate?: string; // 마스킹 후 폐기·미저장 + payMethod?: 'card' | 'easy' | 'bank'; +} + +/** 주차권 — POST/GET 응답 공통 shape. */ +export interface ParkingPassDto { + passNo: string; + status: 'PAID' | 'CANCELLED' | 'USED'; + lotId: string; + lotName: string; + eventId: string | null; + useDate: string; + vehiclePlateMasked: string | null; // 원문 미저장(마스킹만) + amount: number; + payMethod: string | null; + payApprovalNo: string | null; + issuedAt: string; +} + +/** 내 주차권 — GET /api/parking/passes/me. */ +export interface MyPassesDto { + passes: ParkingPassDto[]; +} + +/** 혼잡 영역 — 게이트/주차/인기 공간 공통. occupancyPercent는 계측 부재 시 null. */ +export interface CongestionAreaDto { + id: string; + label: string; + level: CongestionLevel; + levelLabel: string; + occupancyPercent: number | null; +} + +/** 혼잡 요약 — GET /api/public/congestion?eventId= (공개). */ +export interface CongestionOverviewDto { + eventId: string; + overallLevel: CongestionLevel; + overallLabel: string; + onSiteCount: number; + entryGates: CongestionAreaDto[]; + parking: CongestionAreaDto[]; + popularSessions: CongestionAreaDto[]; + updatedAt: string; +} + +// ── B2C 관람객: 라이브 공지 (cms_backlog_contract.md) ── +/** 공지 카테고리 — 배지 색상 매핑(URGENT=red·PROGRAM=amber·INFO=blue·GENERAL=slate). */ +export type LiveNoticeCategory = 'URGENT' | 'PROGRAM' | 'GENERAL' | 'INFO'; + +/** 라이브 공지 — GET /api/public/events/{eventId}/live-notices (공개). */ +export interface LiveNoticeDto { + id: string; + eventId: string; + category: LiveNoticeCategory; + title: string; + body?: string | null; + pinned: boolean; + status: string; // published|archived + authorName?: string | null; + createdAt: string; + updatedAt: string; +} diff --git a/mobile/lib/visitor.ts b/mobile/lib/visitor.ts new file mode 100644 index 0000000..4f6dbb3 --- /dev/null +++ b/mobile/lib/visitor.ts @@ -0,0 +1,47 @@ +/* + * 관람객(B2C) API 배선 — 공개 혼잡/주차/공지 + 인증 주차권. + * 계약: _workspace/parking_congestion_contract.md · src/backend/_workspace/cms_backlog_contract.md. + * 봉투 언랩·인증 헤더는 lib/api.ts가 처리. 공개 조회는 anonymous(토큰 미첨부). + * 주차권 구매/조회는 인증(JWT). PII(차량번호)는 서버가 마스킹해 반환 — 원문 미저장. + */ +import { api } from './api'; +import type { + CongestionOverviewDto, + LiveNoticeDto, + MyPassesDto, + ParkingLotStatusDto, + ParkingPassDto, + PurchasePassRequest, +} from './types'; + +/** 주차 현황 — 공개(비로그인). eventId는 선택. */ +export function getParkingLots(eventId?: string): Promise { + const q = eventId ? `?eventId=${encodeURIComponent(eventId)}` : ''; + return api.get(`/api/public/parking/lots${q}`, { anonymous: true }); +} + +/** 혼잡 요약 — 공개. eventId 필수. */ +export function getCongestion(eventId: string): Promise { + return api.get( + `/api/public/congestion?eventId=${encodeURIComponent(eventId)}`, + { anonymous: true }, + ); +} + +/** 라이브 공지 — 공개. 게시(pinned 우선→최신) 건만. */ +export function getLiveNotices(eventId: string, limit = 100): Promise { + return api.get( + `/api/public/events/${encodeURIComponent(eventId)}/live-notices?limit=${limit}`, + { anonymous: true }, + ); +} + +/** 사전 주차권 구매(mock 결제) — 인증. 구매자 = JWT 주체. */ +export function purchaseParkingPass(req: PurchasePassRequest): Promise { + return api.post('/api/parking/passes', req); +} + +/** 내 주차권 조회 — 인증(본인 소유분만). */ +export function getMyParkingPasses(): Promise { + return api.get('/api/parking/passes/me'); +}