kintex/mobile/app/(tabs)/index.tsx
zio 6c9891c7e2 feat: mobile harness + design v2.1 (84 screens) + PLANNING v3.1 + deliverables + Stitch screens
- Harness: kintex-mobile-dev agent + kintex-mobile-orchestrator skill (WISE mobile ref, Stitch-first design rule, dual app targets B2B/B2C)
- design.md v2.1: full 84-screen inventory (web 51 / admin 10 / public 8 / mobile 15) with Stitch prompts incl. ticketing (SCR-P7/P8, M14/M15)
- PLANNING v3.1: unified account + split signup tracks (2FA required for staff, light signup/guest for visitors), one codebase / two app targets
- Deliverables: dev plan (21s), user/operator/developer guides (17/14/15s), program spec (44s, 65 programs, 8 flowcharts), DA (DB design 14s + table spec xlsx 35 tables/299 cols)
- Benchmark: ticketing-app-benchmark.md (7 apps) -> IMPLEMENTATION_BACKLOG Phase F (14 items)
- Stitch: 23 generated screens saved (mobile 10, admin 6, web core 5, ticket 2)
- mobile/: Expo scaffold (SDK 51, expo-router, secure store JWT)
- frontend: SCR-13~17 QA fixes, icons.tsx, kintexEvents, V10 seed migration
- ci/: KINTEX CI logo assets

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:31:45 +09:00

194 lines
6.8 KiB
TypeScript

/*
* 홈/대시보드 — 활성 행사 헤더(D-데이) + 역할별 진입 카드.
* 워크스페이스(행사)는 로그인 응답에서 수신. 역할에 따라 진입 동선 분기.
*/
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import React from 'react';
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 { useAuth } from '../../context/AuthContext';
import type { EventRole, WorkspaceDto } from '../../lib/types';
import { colors, radius, spacing, type } from '../../theme';
const roleLabel: Record<EventRole, string> = {
ORGANIZER: '주최자',
EXHIBITOR: '참가업체',
CONTRACTOR: '장치·시공업체',
HALL_MANAGER: '홀매니저',
};
export default function HomeScreen() {
const { user, workspaces, activeWorkspace, selectWorkspace } = useAuth();
return (
<ScrollView contentContainerStyle={styles.scroll}>
<Text style={styles.hello}>{user?.displayName ?? '사용자'}, </Text>
{workspaces.length === 0 ? (
<Banner tone="info">
.
</Banner>
) : null}
{/* 행사 선택 */}
{workspaces.length > 1 ? (
<View style={styles.eventPicker}>
{workspaces.map((w) => (
<Pressable
key={w.eventId}
onPress={() => selectWorkspace(w.eventId)}
style={[
styles.eventChip,
activeWorkspace?.eventId === w.eventId && styles.eventChipOn,
]}
>
<Text
style={[
styles.eventChipText,
activeWorkspace?.eventId === w.eventId && styles.eventChipTextOn,
]}
numberOfLines={1}
>
{w.eventName}
</Text>
</Pressable>
))}
</View>
) : null}
{activeWorkspace ? <EventHeader ws={activeWorkspace} /> : null}
{/* 진행 스테퍼 */}
<Card>
<Text style={styles.cardTitle}> </Text>
<Stepper steps={['신청', '승인', '시공', '검수']} active={1} />
</Card>
{/* 역할별 진입 */}
{activeWorkspace ? <RoleActions role={activeWorkspace.myRole} /> : null}
</ScrollView>
);
}
function EventHeader({ ws }: { ws: WorkspaceDto }) {
return (
<Card accent="none">
<View style={styles.eventHeaderTop}>
<Text style={styles.eventName}>{ws.eventName}</Text>
<DdayChip dday={ws.dday} />
</View>
<Text style={styles.eventMeta}>
{ws.hallLabel} · {ws.startDate} ~ {ws.endDate}
</Text>
<View style={styles.roleBadge}>
<Text style={styles.roleBadgeText}>{roleLabel[ws.myRole]}</Text>
</View>
</Card>
);
}
function RoleActions({ role }: { role: EventRole }) {
const actions: { label: string; icon: keyof typeof Ionicons.glyphMap; onPress: () => void }[] = [];
if (role === 'CONTRACTOR') {
actions.push({ label: '현장 체크리스트', icon: 'clipboard-outline', onPress: () => router.push('/checklist') });
}
if (role === 'HALL_MANAGER') {
actions.push({ label: '현장 검수', icon: 'shield-checkmark-outline', onPress: () => router.push('/inspection') });
}
actions.push({ label: '시각화 갤러리', icon: 'images-outline', onPress: () => router.push('/(tabs)/gallery') });
return (
<View style={styles.actions}>
{actions.map((a) => (
<Pressable key={a.label} style={styles.actionCard} onPress={a.onPress}>
<Ionicons name={a.icon} size={26} color={colors.primary600} />
<Text style={styles.actionLabel}>{a.label}</Text>
</Pressable>
))}
</View>
);
}
function Stepper({ steps, active }: { steps: string[]; active: number }) {
return (
<View style={styles.stepper}>
{steps.map((s, i) => (
<View key={s} style={styles.step}>
<View
style={[
styles.stepDot,
i < active && { backgroundColor: colors.success },
i === active && { backgroundColor: colors.primary600 },
]}
>
<Text style={styles.stepDotText}>{i + 1}</Text>
</View>
<Text style={[styles.stepLabel, i === active && { color: colors.primary700, fontWeight: '700' }]}>
{s}
</Text>
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
scroll: { padding: spacing.md, gap: spacing.md },
hello: { fontSize: type.h2.fontSize, fontWeight: '700', color: colors.neutral900 },
eventPicker: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
eventChip: {
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: radius.pill,
borderWidth: 1,
borderColor: colors.neutral200,
maxWidth: 220,
},
eventChipOn: { backgroundColor: colors.primary100, borderColor: colors.primary600 },
eventChipText: { color: colors.neutral700, fontSize: type.caption.fontSize },
eventChipTextOn: { color: colors.primary700, fontWeight: '700' },
eventHeaderTop: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 },
eventName: { flex: 1, fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 },
eventMeta: { marginTop: 4, color: colors.neutral500, fontSize: type.caption.fontSize },
roleBadge: {
alignSelf: 'flex-start',
marginTop: 10,
backgroundColor: colors.primary050,
borderRadius: radius.pill,
paddingHorizontal: 10,
paddingVertical: 3,
},
roleBadgeText: { color: colors.primary700, fontSize: type.caption.fontSize, fontWeight: '600' },
cardTitle: { fontSize: type.h3.fontSize, fontWeight: '600', color: colors.neutral900, marginBottom: 12 },
actions: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.md },
actionCard: {
flexGrow: 1,
flexBasis: '45%',
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.neutral200,
borderRadius: radius.md,
padding: spacing.md,
gap: 8,
minHeight: 96,
justifyContent: 'center',
},
actionLabel: { fontSize: type.body.fontSize, fontWeight: '600', color: colors.neutral700 },
stepper: { flexDirection: 'row', justifyContent: 'space-between' },
step: { alignItems: 'center', flex: 1, gap: 6 },
stepDot: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.neutral200,
alignItems: 'center',
justifyContent: 'center',
},
stepDotText: { color: colors.white, fontWeight: '700', fontSize: type.caption.fontSize },
stepLabel: { fontSize: type.caption.fontSize, color: colors.neutral500 },
});