feat: 보험케어 앱 초기 구축 (React Native + Expo)

- 14개 핵심 기능 화면 구현 (진단/점수/청구/가족/AI판정 등)
- 하단 탭 5개 (홈/내보험/보험금/상담/마이) — 시그널플래너/보맵 참고
- 공용 컴포넌트, 테마 시스템, Zustand 전역 스토어
- Android/iOS/Web 크로스플랫폼 지원

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-04-22 23:54:38 +09:00
commit 0a111c172f
44 changed files with 18845 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
node_modules/
.expo/
.expo-shared/
dist/
web-build/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
.DS_Store
.env
.env.local
*.log
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationContainer } from '@react-navigation/native';
import RootNavigator from './src/navigation/RootNavigator';
export default function App() {
return (
<SafeAreaProvider>
<NavigationContainer>
<StatusBar style="dark" />
<RootNavigator />
</NavigationContainer>
</SafeAreaProvider>
);
}
+99
View File
@@ -0,0 +1,99 @@
# 보험케어 (Insurance Care)
시그널플래너 / 보맵 벤치마크 기반 보험 관리 앱.
**React Native + Expo**로 **Android · iOS · Web** 모두 지원합니다.
## 🚀 실행 방법
### 1. 의존성 설치
```bash
npm install
```
### 2. 앱 실행
```bash
npm start # Expo Dev Tools 실행 (QR 코드로 실기기 연결)
npm run android # Android 에뮬레이터 또는 연결된 기기
npm run ios # iOS 시뮬레이터 (macOS 필요)
npm run web # 브라우저에서 미리보기 (디자인 확인용)
```
개발 중 UI 빠르게 확인할 때는 `npm run web`이 가장 편합니다.
실기기는 Expo Go 앱 설치 후 QR 스캔.
## 📱 주요 구성
### 하단 탭 (시그널플래너/보맵 스타일)
| 탭 | 설명 |
|---|---|
| 홈 | 보험 점수, 숨은보험금, 주요 기능 바로가기 |
| 내 보험 | 가입된 보험 목록·월 납입·총 보장 |
| 보험금 | 청구·AI 판정·숨은보험금·체크리스트 |
| 상담 | 카카오/전화/방문 상담, 추천 설계사 |
| 마이 | 내 정보, 설정, 고객센터 |
### 구현된 14개 핵심 기능
| # | 기능 | 라우트 |
|---|---|---|
| 1 | 3초 보험 진단 (단계형 설문) | `Diagnosis` |
| 2 | 연령별 분석 (차트 + 추천) | `Analysis` |
| 3 | 내 보험 점수 (원형 게이지) | `Score` |
| 4 | 전문가 상담 (카카오/전화/방문) | `Consult` |
| 5 | 숨은보험금 조회 (로딩→결과) | `HiddenMoney` |
| 6 | 질병코드 조회 (검색 + DB) | `DiseaseCode` |
| 7 | 보험금 청구 (카메라/갤러리) | `Claim` |
| 8 | 건강검진 결과 분석 | `HealthCheck` |
| 9 | 우리 가족 보험 한눈에 | `Family` |
| 10 | 병원 가기 전 체크리스트 | `HospitalChecklist` |
| 11 | AI 보험금 판정 (챗봇형) | `AIJudge` |
| 12 | 보험료 다이어트 진단 | `PremiumDiet` |
| 13 | 실손 세대 자동 판별 | `SilsonGen` |
| 14 | 만기·갱신 알림 (알림톡 설정) | `Notifications` |
## 🧩 기술 스택
- **React Native 0.74** + **Expo 51**
- **TypeScript**
- **React Navigation 6** (Native Stack + Bottom Tabs)
- **Zustand** — 전역 상태 (프로필/가족/보험 데이터)
- **expo-linear-gradient**, **react-native-svg** — 그라데이션/점수 게이지
- **react-native-chart-kit** — 연령별 보험료 막대 차트
- **expo-image-picker** — 영수증 카메라/갤러리 (보험금 청구)
- **@expo/vector-icons** — Ionicons
## 📂 폴더 구조
```
src/
├── components/ # 공용 UI (Card, Button, Header, Badge, ScoreGauge 등)
├── data/ # 질병 코드 DB, 실손 세대 테이블 (목업)
├── navigation/ # RootNavigator, BottomTabs
├── screens/ # 화면 19개 (하단 탭 5 + 스택 14)
├── store/ # Zustand 전역 스토어
└── theme/ # colors, typography, spacing, radius, shadow
```
## 🎨 디자인 시스템
- **Primary**: `#3B82F6` (파란 계열 / 보험 앱 관례)
- **Success**: `#10B981` / **Warning**: `#F59E0B` / **Danger**: `#EF4444`
- **폰트**: 시스템 폰트 (SF Pro / Roboto / 나눔고딕)
- 라운드 14px, 그림자 3단계, Pill/Badge/Progress 컴포넌트 제공
## 📝 TODO (다음 단계)
- [ ] 실제 보험사 OpenAPI 연동 (현재 목업 데이터)
- [ ] 카카오 알림톡 발송 백엔드 (Solapi/Kakao Business API)
- [ ] 푸시 알림 스케줄링 (expo-notifications)
- [ ] 사용자 인증 (소셜 로그인 — 카카오/네이버/애플)
- [ ] 보험 증권 OCR (네이버 Clova / Google Vision)
- [ ] AI 판정 기능 실제 LLM 연동 (Claude API)
- [ ] 앱스토어/플레이스토어 배포 (EAS Build)
## 💡 웹에서도 확인되나요?
네. `npm run web` 실행 시 모든 화면을 브라우저에서 볼 수 있습니다.
단, 카메라(보험금 청구)/푸시알림 같은 네이티브 기능은 실기기/에뮬레이터에서 확인해야 합니다.
+44
View File
@@ -0,0 +1,44 @@
{
"expo": {
"name": "보험케어",
"slug": "insurance-care",
"version": "1.0.0",
"orientation": "portrait",
"userInterfaceStyle": "light",
"splash": {
"resizeMode": "contain",
"backgroundColor": "#3B82F6"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.insurancecare.app",
"infoPlist": {
"NSCameraUsageDescription": "보험금 청구를 위한 영수증/진단서 촬영에 카메라가 사용됩니다.",
"NSPhotoLibraryUsageDescription": "보험금 청구 서류 첨부를 위해 사진 접근이 필요합니다."
}
},
"android": {
"package": "com.insurancecare.app",
"permissions": [
"CAMERA",
"READ_EXTERNAL_STORAGE",
"WRITE_EXTERNAL_STORAGE",
"POST_NOTIFICATIONS"
]
},
"web": {
"bundler": "metro"
},
"plugins": [
[
"expo-image-picker",
{
"cameraPermission": "보험금 청구 시 영수증/진단서 촬영을 위해 카메라를 사용합니다.",
"photosPermission": "보험금 청구 서류 첨부를 위해 사진 접근이 필요합니다."
}
],
"expo-notifications"
]
}
}
+18
View File
@@ -0,0 +1,18 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [
[
'module-resolver',
{
root: ['./'],
alias: {
'@': './src',
},
},
],
'react-native-reanimated/plugin',
],
};
};
+14704
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
{
"name": "insurance-care",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@expo/metro-runtime": "~3.2.1",
"@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/bottom-tabs": "^6.5.20",
"@react-navigation/native": "^6.1.17",
"@react-navigation/native-stack": "^6.9.26",
"expo": "~51.0.0",
"expo-device": "~6.0.2",
"expo-font": "^55.0.6",
"expo-image-picker": "~15.0.5",
"expo-linear-gradient": "~13.0.2",
"expo-notifications": "~0.28.0",
"expo-status-bar": "~1.12.1",
"react": "18.2.0",
"react-dom": "^18.2.0",
"react-native": "0.74.3",
"react-native-chart-kit": "^6.12.0",
"react-native-gesture-handler": "~2.16.1",
"react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1",
"react-native-svg": "15.2.0",
"react-native-web": "^0.19.13",
"zustand": "^4.5.2"
},
"devDependencies": {
"@babel/core": "^7.24.0",
"@types/react": "~18.2.45",
"babel-plugin-module-resolver": "^5.0.2",
"typescript": "~5.3.3"
},
"private": true
}
+39
View File
@@ -0,0 +1,39 @@
import React from 'react';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
import { colors } from '@/theme/colors';
import { radius, typography } from '@/theme/typography';
type Tone = 'primary' | 'success' | 'warning' | 'danger' | 'neutral';
type Props = {
label: string;
tone?: Tone;
style?: ViewStyle;
};
export default function Badge({ label, tone = 'primary', style }: Props) {
const { bg, color } = toneMap[tone];
return (
<View style={[styles.base, { backgroundColor: bg }, style]}>
<Text style={[styles.label, { color }]}>{label}</Text>
</View>
);
}
const toneMap: Record<Tone, { bg: string; color: string }> = {
primary: { bg: colors.primaryLight, color: colors.primaryDark },
success: { bg: colors.secondaryLight, color: colors.secondary },
warning: { bg: colors.warningLight, color: colors.warning },
danger: { bg: colors.dangerLight, color: colors.danger },
neutral: { bg: colors.surfaceAlt, color: colors.textSecondary },
};
const styles = StyleSheet.create({
base: {
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: radius.pill,
alignSelf: 'flex-start',
},
label: { ...typography.small, fontWeight: '700' },
});
+110
View File
@@ -0,0 +1,110 @@
import React from 'react';
import { Text, StyleSheet, TouchableOpacity, ViewStyle, ActivityIndicator } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
type Variant = 'primary' | 'secondary' | 'outline' | 'kakao' | 'ghost' | 'danger';
type Props = {
title: string;
onPress?: () => void;
variant?: Variant;
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
style?: ViewStyle | ViewStyle[];
fullWidth?: boolean;
leftIcon?: React.ReactNode;
};
export default function Button({
title,
onPress,
variant = 'primary',
size = 'md',
disabled,
loading,
style,
fullWidth,
leftIcon,
}: Props) {
const height = size === 'sm' ? 38 : size === 'lg' ? 56 : 48;
const fontSize = size === 'sm' ? 14 : size === 'lg' ? 17 : 15;
const baseStyle: ViewStyle = {
height,
borderRadius: radius.md,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.xl,
flexDirection: 'row',
opacity: disabled ? 0.5 : 1,
...(fullWidth ? { alignSelf: 'stretch' } : {}),
};
const content = (
<>
{loading ? (
<ActivityIndicator color={variant === 'outline' || variant === 'ghost' ? colors.primary : '#FFF'} />
) : (
<>
{leftIcon ? <Text style={{ marginRight: 6 }}>{leftIcon}</Text> : null}
<Text style={[getTextStyle(variant), { fontSize }]}>{title}</Text>
</>
)}
</>
);
if (variant === 'primary') {
return (
<TouchableOpacity activeOpacity={0.85} onPress={onPress} disabled={disabled || loading} style={[baseStyle, style]}>
<LinearGradient
colors={colors.gradient.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={StyleSheet.absoluteFill as ViewStyle}
/>
{content}
</TouchableOpacity>
);
}
const variantStyle: ViewStyle =
variant === 'secondary'
? { backgroundColor: colors.surfaceAlt }
: variant === 'outline'
? { backgroundColor: colors.surface, borderWidth: 1.5, borderColor: colors.primary }
: variant === 'kakao'
? { backgroundColor: colors.kakao }
: variant === 'danger'
? { backgroundColor: colors.danger }
: { backgroundColor: 'transparent' };
return (
<TouchableOpacity
activeOpacity={0.85}
onPress={onPress}
disabled={disabled || loading}
style={[baseStyle, variantStyle, style]}
>
{content}
</TouchableOpacity>
);
}
function getTextStyle(variant: Variant) {
const base = { ...typography.button, fontWeight: '700' as const };
switch (variant) {
case 'primary':
case 'danger':
return { ...base, color: '#FFF' };
case 'secondary':
return { ...base, color: colors.text };
case 'outline':
case 'ghost':
return { ...base, color: colors.primary };
case 'kakao':
return { ...base, color: colors.kakaoText };
}
}
+53
View File
@@ -0,0 +1,53 @@
import React from 'react';
import { View, StyleSheet, ViewProps, ViewStyle, TouchableOpacity } from 'react-native';
import { colors } from '@/theme/colors';
import { radius, shadow, spacing } from '@/theme/typography';
type Props = ViewProps & {
style?: ViewStyle | ViewStyle[];
onPress?: () => void;
variant?: 'default' | 'flat' | 'outlined';
padding?: keyof typeof spacing | number;
};
export default function Card({ children, style, onPress, variant = 'default', padding = 'lg', ...rest }: Props) {
const padValue = typeof padding === 'number' ? padding : spacing[padding];
const boxStyle = [
styles.base,
variant === 'default' && styles.default,
variant === 'flat' && styles.flat,
variant === 'outlined' && styles.outlined,
{ padding: padValue },
style,
];
if (onPress) {
return (
<TouchableOpacity activeOpacity={0.85} onPress={onPress} style={boxStyle} {...rest}>
{children}
</TouchableOpacity>
);
}
return (
<View style={boxStyle} {...rest}>
{children}
</View>
);
}
const styles = StyleSheet.create({
base: {
backgroundColor: colors.surface,
borderRadius: radius.lg,
},
default: {
...shadow.md,
},
flat: {
backgroundColor: colors.surfaceAlt,
},
outlined: {
borderWidth: 1,
borderColor: colors.border,
},
});
+53
View File
@@ -0,0 +1,53 @@
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import { colors } from '@/theme/colors';
import { spacing, typography } from '@/theme/typography';
type Props = {
title: string;
subtitle?: string;
showBack?: boolean;
right?: React.ReactNode;
style?: ViewStyle;
};
export default function Header({ title, subtitle, showBack = true, right, style }: Props) {
const navigation = useNavigation();
return (
<View style={[styles.wrap, style]}>
<View style={styles.left}>
{showBack ? (
<TouchableOpacity onPress={() => navigation.goBack()} hitSlop={12}>
<Ionicons name="chevron-back" size={26} color={colors.text} />
</TouchableOpacity>
) : null}
</View>
<View style={styles.center}>
<Text style={styles.title} numberOfLines={1}>
{title}
</Text>
{subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : null}
</View>
<View style={styles.right}>{right}</View>
</View>
);
}
const styles = StyleSheet.create({
wrap: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
backgroundColor: colors.surface,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
left: { width: 40, alignItems: 'flex-start' },
right: { width: 40, alignItems: 'flex-end' },
center: { flex: 1, alignItems: 'center' },
title: { ...typography.title, color: colors.text },
subtitle: { ...typography.small, color: colors.textSecondary, marginTop: 2 },
});
+67
View File
@@ -0,0 +1,67 @@
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
type Props = {
icon: keyof typeof Ionicons.glyphMap;
label: string;
onPress?: () => void;
color?: string;
bg?: string;
badge?: string;
};
export default function IconTile({ icon, label, onPress, color = colors.primary, bg = colors.primaryLight, badge }: Props) {
return (
<TouchableOpacity activeOpacity={0.8} onPress={onPress} style={styles.wrap}>
<View style={[styles.iconBox, { backgroundColor: bg }]}>
<Ionicons name={icon} size={24} color={color} />
{badge ? (
<View style={styles.badge}>
<Text style={styles.badgeText}>{badge}</Text>
</View>
) : null}
</View>
<Text style={styles.label} numberOfLines={2}>
{label}
</Text>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
wrap: {
alignItems: 'center',
width: '25%',
paddingVertical: spacing.sm,
},
iconBox: {
width: 56,
height: 56,
borderRadius: radius.lg,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 8,
},
label: {
...typography.small,
color: colors.text,
textAlign: 'center',
fontWeight: '500',
paddingHorizontal: 4,
},
badge: {
position: 'absolute',
top: -4,
right: -4,
backgroundColor: colors.danger,
borderRadius: 10,
paddingHorizontal: 6,
paddingVertical: 2,
minWidth: 18,
alignItems: 'center',
},
badgeText: { color: '#FFF', fontSize: 10, fontWeight: '700' },
});
+32
View File
@@ -0,0 +1,32 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { colors } from '@/theme/colors';
import { radius } from '@/theme/typography';
type Props = {
value: number; // 0-100
height?: number;
color?: string;
track?: string;
};
export default function ProgressBar({ value, height = 8, color = colors.primary, track = colors.surfaceAlt }: Props) {
const pct = Math.max(0, Math.min(100, value));
return (
<View style={[styles.track, { height, backgroundColor: track }]}>
<View style={[styles.fill, { width: `${pct}%`, backgroundColor: color }]} />
</View>
);
}
const styles = StyleSheet.create({
track: {
width: '100%',
borderRadius: radius.pill,
overflow: 'hidden',
},
fill: {
height: '100%',
borderRadius: radius.pill,
},
});
+55
View File
@@ -0,0 +1,55 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
type Props = {
value: number; // 0-100
size?: number;
label?: string;
};
export default function ScoreGauge({ value, size = 180, label = '내 보험 점수' }: Props) {
const stroke = 14;
const radius = (size - stroke) / 2;
const circ = 2 * Math.PI * radius;
const pct = Math.max(0, Math.min(100, value));
const dash = circ * (pct / 100);
const color = pct >= 80 ? colors.success : pct >= 60 ? colors.accent : colors.danger;
return (
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
<Svg width={size} height={size}>
<Circle cx={size / 2} cy={size / 2} r={radius} stroke={colors.surfaceAlt} strokeWidth={stroke} fill="none" />
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={color}
strokeWidth={stroke}
fill="none"
strokeLinecap="round"
strokeDasharray={`${dash}, ${circ}`}
transform={`rotate(-90 ${size / 2} ${size / 2})`}
/>
</Svg>
<View style={styles.center}>
<Text style={styles.label}>{label}</Text>
<Text style={[styles.value, { color }]}>{pct}</Text>
<Text style={styles.unit}>/ 100</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
center: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
},
label: { ...typography.caption, color: colors.textSecondary },
value: { fontSize: 48, fontWeight: '800', marginTop: 2 },
unit: { ...typography.small, color: colors.textTertiary },
});
+45
View File
@@ -0,0 +1,45 @@
import React from 'react';
import { ScrollView, View, StyleSheet, ViewStyle, ScrollViewProps, StatusBar } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { colors } from '@/theme/colors';
type Props = ScrollViewProps & {
children: React.ReactNode;
scroll?: boolean;
style?: ViewStyle;
contentStyle?: ViewStyle;
background?: string;
edges?: Array<'top' | 'right' | 'bottom' | 'left'>;
};
export default function ScreenContainer({
children,
scroll = true,
style,
contentStyle,
background = colors.background,
edges = ['top', 'left', 'right'],
...rest
}: Props) {
return (
<SafeAreaView style={[styles.safe, { backgroundColor: background }, style]} edges={edges}>
<StatusBar barStyle="dark-content" />
{scroll ? (
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={[{ paddingBottom: 40 }, contentStyle]}
showsVerticalScrollIndicator={false}
{...rest}
>
{children}
</ScrollView>
) : (
<View style={[{ flex: 1 }, contentStyle]}>{children}</View>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1 },
});
+43
View File
@@ -0,0 +1,43 @@
import React from 'react';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
import { colors } from '@/theme/colors';
import { spacing, typography } from '@/theme/typography';
type Props = {
title?: string;
subtitle?: string;
right?: React.ReactNode;
children: React.ReactNode;
style?: ViewStyle;
};
export default function Section({ title, subtitle, right, children, style }: Props) {
return (
<View style={[styles.wrap, style]}>
{(title || right) && (
<View style={styles.head}>
<View style={{ flex: 1 }}>
{title ? <Text style={styles.title}>{title}</Text> : null}
{subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : null}
</View>
{right}
</View>
)}
<View>{children}</View>
</View>
);
}
const styles = StyleSheet.create({
wrap: {
marginTop: spacing.xl,
paddingHorizontal: spacing.lg,
},
head: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.md,
},
title: { ...typography.h3, color: colors.text },
subtitle: { ...typography.caption, color: colors.textSecondary, marginTop: 2 },
});
+38
View File
@@ -0,0 +1,38 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { colors } from '@/theme/colors';
import { spacing, typography } from '@/theme/typography';
type Props = {
label: string;
value: string;
hint?: string;
tone?: 'default' | 'success' | 'warning' | 'danger';
};
export default function StatRow({ label, value, hint, tone = 'default' }: Props) {
const toneColor =
tone === 'success' ? colors.success : tone === 'warning' ? colors.warning : tone === 'danger' ? colors.danger : colors.text;
return (
<View style={styles.row}>
<View style={{ flex: 1 }}>
<Text style={styles.label}>{label}</Text>
{hint ? <Text style={styles.hint}>{hint}</Text> : null}
</View>
<Text style={[styles.value, { color: toneColor }]}>{value}</Text>
</View>
);
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
label: { ...typography.body, color: colors.text },
hint: { ...typography.small, color: colors.textSecondary, marginTop: 2 },
value: { ...typography.bodyBold, color: colors.text },
});
+111
View File
@@ -0,0 +1,111 @@
export type DiseaseMatch = {
code: string;
name: string;
keywords: string[];
category: '암' | '뇌혈관' | '심장' | '근골격' | '호흡기' | '소화기' | '피부' | '여성' | '기타';
coverage: Array<{ policy: string; amount: string; note?: string }>;
tips?: string[];
};
export const diseaseDB: DiseaseMatch[] = [
{
code: 'D34',
name: '갑상선 양성 신생물 (결절)',
keywords: ['갑상선', '결절', '갑상선 결절', '목 혹'],
category: '기타',
coverage: [
{ policy: '암보험 소액암 진단비', amount: '300~1,000만원', note: '경계성 종양 조건 확인 필요' },
{ policy: '실손의료비', amount: '검사비/조직검사 청구 가능' },
],
tips: ['조직검사 결과지(병리조직검사 보고서) 반드시 수령', '암 진단비는 C73 코드로 변경 시 전액 지급'],
},
{
code: 'C50',
name: '유방암',
keywords: ['유방암', '유방', '유방 종양'],
category: '암',
coverage: [
{ policy: '일반암 진단비', amount: '3,000~5,000만원' },
{ policy: '여성특정질병 특약', amount: '여성암 추가 가산' },
{ policy: '실손', amount: '항암치료/MRI 전액 청구' },
],
},
{
code: 'C73',
name: '갑상선암',
keywords: ['갑상선암'],
category: '암',
coverage: [{ policy: '소액암(유사암) 진단비', amount: '가입금액의 10~20%' }],
tips: ['대부분 소액암으로 분류 — 일반암 진단비 전액 지급 불가'],
},
{
code: 'S93',
name: '발목 염좌',
keywords: ['발목', '삠', '염좌', '접질림'],
category: '근골격',
coverage: [
{ policy: '실손의료비', amount: '진료비/물리치료비 전액' },
{ policy: '상해보험', amount: '통원일당 1~5만원' },
],
tips: ['6개월 이상 치료 시 후유장해 평가 가능'],
},
{
code: 'I63',
name: '뇌경색 (뇌졸중)',
keywords: ['뇌경색', '뇌졸중', '뇌혈관'],
category: '뇌혈관',
coverage: [
{ policy: '뇌혈관질환 진단비', amount: '2,000~5,000만원' },
{ policy: '뇌졸중 진단비', amount: '1,000만원' },
],
},
{
code: 'I21',
name: '급성 심근경색',
keywords: ['심근경색', '심장마비'],
category: '심장',
coverage: [{ policy: '허혈성심장질환 진단비', amount: '2,000~5,000만원' }],
},
{
code: 'K29',
name: '위염 / 역류성 식도염',
keywords: ['위염', '식도염', '역류성'],
category: '소화기',
coverage: [{ policy: '실손', amount: '진료비/약제비 청구 가능' }],
},
{
code: 'L70',
name: '여드름',
keywords: ['여드름', '트러블'],
category: '피부',
coverage: [{ policy: '실손 제외 (미용 목적)', amount: '보장 불가' }],
},
{
code: 'N80',
name: '자궁내막증',
keywords: ['자궁내막증'],
category: '여성',
coverage: [
{ policy: '여성특정질병', amount: '100~500만원' },
{ policy: '실손', amount: '진료/수술비 청구' },
],
},
{
code: 'J00',
name: '감기 / 상기도 감염',
keywords: ['감기', '기침', '콧물'],
category: '호흡기',
coverage: [{ policy: '실손 통원', amount: '1회 1만원 공제 후 80%' }],
},
];
export function searchDisease(query: string): DiseaseMatch[] {
const q = query.trim().toLowerCase();
if (!q) return [];
return diseaseDB.filter(
(d) =>
d.name.toLowerCase().includes(q) ||
d.code.toLowerCase().includes(q) ||
d.keywords.some((k) => k.toLowerCase().includes(q))
);
}
+68
View File
@@ -0,0 +1,68 @@
export type SilsonGen = 1 | 2 | 3 | 4 | 5;
export type SilsonGenInfo = {
generation: SilsonGen;
label: string;
period: string;
pros: string[];
cons: string[];
selfPay: string;
renewCycle: string;
};
export const silsonGenTable: SilsonGenInfo[] = [
{
generation: 1,
label: '1세대 (구 실손)',
period: '~2009.09',
pros: ['자기부담금 0%', '보장 범위 가장 넓음'],
cons: ['보험료 인상률 매우 높음', '신규 가입 불가'],
selfPay: '0%',
renewCycle: '3~5년',
},
{
generation: 2,
label: '2세대 (표준화 실손)',
period: '2009.10 ~ 2017.03',
pros: ['보장 범위 넓음', '자기부담 10%'],
cons: ['보험료 갱신 시 인상률 높음', '도수치료 제한'],
selfPay: '10%',
renewCycle: '1년',
},
{
generation: 3,
label: '3세대 (착한 실손)',
period: '2017.04 ~ 2021.06',
pros: ['보험료 저렴', '기본형/특약 분리'],
cons: ['비급여 3대 특약 분리', '보장 축소'],
selfPay: '10~20%',
renewCycle: '1년',
},
{
generation: 4,
label: '4세대 실손',
period: '2021.07 ~ 2024.12',
pros: ['보험료 가장 저렴', '비급여 할인/할증'],
cons: ['자기부담금 상승', '비급여 사용 시 보험료 인상'],
selfPay: '급여 20% / 비급여 30%',
renewCycle: '1년',
},
{
generation: 5,
label: '5세대 실손',
period: '2025.01 ~',
pros: ['4세대 대비 월 보험료 최대 70% 절감'],
cons: ['보장 축소 (도수/비급여 주사/MRI 제한)', '전환 시 복귀 불가'],
selfPay: '급여 20% / 비급여 50%',
renewCycle: '1년',
},
];
export function identifyGenFromDate(joinDate: string): SilsonGen {
const d = new Date(joinDate);
if (d < new Date('2009-10-01')) return 1;
if (d < new Date('2017-04-01')) return 2;
if (d < new Date('2021-07-01')) return 3;
if (d < new Date('2025-01-01')) return 4;
return 5;
}
+46
View File
@@ -0,0 +1,46 @@
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import HomeScreen from '@/screens/HomeScreen';
import MyInsuranceScreen from '@/screens/MyInsuranceScreen';
import ClaimHubScreen from '@/screens/ClaimHubScreen';
import ConsultHubScreen from '@/screens/ConsultHubScreen';
import MyPageScreen from '@/screens/MyPageScreen';
import { colors } from '@/theme/colors';
const Tab = createBottomTabNavigator();
export default function BottomTabs() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
headerShown: false,
tabBarActiveTintColor: colors.primary,
tabBarInactiveTintColor: colors.textTertiary,
tabBarStyle: {
height: 62,
paddingBottom: 8,
paddingTop: 6,
borderTopColor: colors.border,
},
tabBarLabelStyle: { fontSize: 11, fontWeight: '600' },
tabBarIcon: ({ color, size, focused }) => {
const icon: Record<string, keyof typeof Ionicons.glyphMap> = {
Home: focused ? 'home' : 'home-outline',
MyInsurance: focused ? 'shield-checkmark' : 'shield-checkmark-outline',
ClaimHub: focused ? 'receipt' : 'receipt-outline',
ConsultHub: focused ? 'chatbubbles' : 'chatbubbles-outline',
MyPage: focused ? 'person' : 'person-outline',
};
return <Ionicons name={icon[route.name]} size={size} color={color} />;
},
})}
>
<Tab.Screen name="Home" component={HomeScreen} options={{ tabBarLabel: '홈' }} />
<Tab.Screen name="MyInsurance" component={MyInsuranceScreen} options={{ tabBarLabel: '내 보험' }} />
<Tab.Screen name="ClaimHub" component={ClaimHubScreen} options={{ tabBarLabel: '보험금' }} />
<Tab.Screen name="ConsultHub" component={ConsultHubScreen} options={{ tabBarLabel: '상담' }} />
<Tab.Screen name="MyPage" component={MyPageScreen} options={{ tabBarLabel: '마이' }} />
</Tab.Navigator>
);
}
+59
View File
@@ -0,0 +1,59 @@
import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import BottomTabs from './BottomTabs';
import DiagnosisScreen from '@/screens/DiagnosisScreen';
import AnalysisScreen from '@/screens/AnalysisScreen';
import ScoreScreen from '@/screens/ScoreScreen';
import ConsultScreen from '@/screens/ConsultScreen';
import HiddenMoneyScreen from '@/screens/HiddenMoneyScreen';
import DiseaseCodeScreen from '@/screens/DiseaseCodeScreen';
import ClaimScreen from '@/screens/ClaimScreen';
import HealthCheckScreen from '@/screens/HealthCheckScreen';
import FamilyScreen from '@/screens/FamilyScreen';
import HospitalChecklistScreen from '@/screens/HospitalChecklistScreen';
import AIJudgeScreen from '@/screens/AIJudgeScreen';
import PremiumDietScreen from '@/screens/PremiumDietScreen';
import SilsonGenScreen from '@/screens/SilsonGenScreen';
import NotificationScreen from '@/screens/NotificationScreen';
export type RootStackParamList = {
Tabs: undefined;
Diagnosis: undefined;
Analysis: undefined;
Score: undefined;
Consult: undefined;
HiddenMoney: undefined;
DiseaseCode: undefined;
Claim: undefined;
HealthCheck: undefined;
Family: undefined;
HospitalChecklist: undefined;
AIJudge: undefined;
PremiumDiet: undefined;
SilsonGen: undefined;
Notifications: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function RootNavigator() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Tabs" component={BottomTabs} />
<Stack.Screen name="Diagnosis" component={DiagnosisScreen} />
<Stack.Screen name="Analysis" component={AnalysisScreen} />
<Stack.Screen name="Score" component={ScoreScreen} />
<Stack.Screen name="Consult" component={ConsultScreen} />
<Stack.Screen name="HiddenMoney" component={HiddenMoneyScreen} />
<Stack.Screen name="DiseaseCode" component={DiseaseCodeScreen} />
<Stack.Screen name="Claim" component={ClaimScreen} />
<Stack.Screen name="HealthCheck" component={HealthCheckScreen} />
<Stack.Screen name="Family" component={FamilyScreen} />
<Stack.Screen name="HospitalChecklist" component={HospitalChecklistScreen} />
<Stack.Screen name="AIJudge" component={AIJudgeScreen} />
<Stack.Screen name="PremiumDiet" component={PremiumDietScreen} />
<Stack.Screen name="SilsonGen" component={SilsonGenScreen} />
<Stack.Screen name="Notifications" component={NotificationScreen} />
</Stack.Navigator>
);
}
+250
View File
@@ -0,0 +1,250 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, TextInput, TouchableOpacity, ActivityIndicator } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import Badge from '@/components/Badge';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
type Msg = { role: 'user' | 'ai'; text: string; verdict?: Verdict };
type Verdict = {
available: boolean;
policies: Array<{ name: string; desc: string }>;
docs: string[];
estimated: string;
caution?: string;
};
const samples = [
'어제 계단에서 넘어져서 발목 삐었어요',
'감기로 병원 다녀왔는데 청구 되나요?',
'대장내시경에서 용종 2개 제거했어요',
'어깨가 아파서 도수치료 5회 받았습니다',
];
function judge(input: string): Verdict {
const q = input.toLowerCase();
if (q.includes('발목') || q.includes('삐') || q.includes('넘어')) {
return {
available: true,
policies: [
{ name: '실손의료비', desc: '정형외과 진료비 청구 가능' },
{ name: '상해보험 통원일당', desc: '1일 1~5만원 (가입 금액 따라)' },
],
docs: ['정형외과 영수증', '진단서 (S93 발목 염좌)'],
estimated: '5~15만원',
};
}
if (q.includes('감기')) {
return {
available: true,
policies: [{ name: '실손 통원의료비', desc: '1회 1만원 공제 후 80% 보장' }],
docs: ['병원 영수증', '처방전'],
estimated: '2~4만원',
caution: '실손 외래 통원 건당 자기부담금(의원 1만원, 종합병원 2만원) 공제',
};
}
if (q.includes('용종')) {
return {
available: true,
policies: [
{ name: '실손의료비', desc: '내시경/제거 시술비 청구' },
{ name: '수술비 특약', desc: '1종 수술 해당 - 10~50만원' },
],
docs: ['수술확인서 (용종절제술)', '세부내역서', '조직검사 결과지'],
estimated: '15~50만원',
caution: '조직검사 결과 악성으로 판정 시 암진단비 별도 청구 가능',
};
}
if (q.includes('도수치료')) {
return {
available: true,
policies: [{ name: '실손 비급여 특약', desc: '도수치료 1회 25만원 한도' }],
docs: ['병원 영수증 (세부내역서 포함)', '의사 소견서'],
estimated: '회당 3~25만원 (실손 세대별 상이)',
caution: '4세대 이후 연 10회 또는 50만원 한도 제한',
};
}
return {
available: false,
policies: [],
docs: [],
estimated: '-',
caution: '더 구체적인 증상/시술명을 알려주시면 정확히 판정해 드릴 수 있어요.',
};
}
export default function AIJudgeScreen() {
const [msgs, setMsgs] = useState<Msg[]>([
{ role: 'ai', text: '안녕하세요! 보험금 청구 가능 여부를 AI가 판정해 드려요. 증상이나 시술명을 편하게 말씀해 주세요.' },
]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const send = (text: string) => {
if (!text.trim()) return;
setMsgs((m) => [...m, { role: 'user', text }]);
setInput('');
setLoading(true);
setTimeout(() => {
const v = judge(text);
setMsgs((m) => [
...m,
{
role: 'ai',
text: v.available ? '✅ 청구 가능합니다! 아래 내용 참고해 주세요.' : 'ℹ️ 더 자세한 정보가 필요해요.',
verdict: v,
},
]);
setLoading(false);
}, 1100);
};
return (
<ScreenContainer>
<Header title="AI 보험금 판정" />
<View style={{ padding: spacing.lg }}>
<LinearGradient colors={['#8B5CF6', '#7C3AED']} style={styles.hero}>
<Ionicons name="sparkles" size={32} color="#FFF" />
<Text style={styles.heroTitle}>🤖 AI가 </Text>
<Text style={styles.heroSub}> !</Text>
</LinearGradient>
<View style={{ marginTop: 16 }}>
{msgs.map((m, i) => (
<View key={i} style={[styles.msg, m.role === 'user' ? styles.msgUser : styles.msgAI]}>
<Text style={{ color: m.role === 'user' ? '#FFF' : colors.text, ...typography.body }}>{m.text}</Text>
{m.verdict && <VerdictCard v={m.verdict} />}
</View>
))}
{loading && (
<View style={[styles.msg, styles.msgAI]}>
<ActivityIndicator color={colors.primary} />
</View>
)}
</View>
<Section title="예시 질문">
<View style={{ gap: 8 }}>
{samples.map((s) => (
<TouchableOpacity key={s} style={styles.sample} onPress={() => send(s)}>
<Ionicons name="chatbubble-outline" size={16} color={colors.primary} />
<Text style={{ marginLeft: 8, ...typography.body, color: colors.text } as any}>{s}</Text>
</TouchableOpacity>
))}
</View>
</Section>
<Card style={{ marginTop: 16 }}>
<View style={styles.inputRow}>
<TextInput
style={styles.input}
placeholder="증상이나 시술명을 입력하세요"
value={input}
onChangeText={setInput}
placeholderTextColor={colors.textTertiary}
multiline
/>
<TouchableOpacity style={styles.sendBtn} onPress={() => send(input)}>
<Ionicons name="arrow-up" size={20} color="#FFF" />
</TouchableOpacity>
</View>
</Card>
<Text style={styles.disclaimer}>
AI
</Text>
<View style={{ height: 24 }} />
<Button title="실제 청구하러 가기" onPress={() => {}} />
</View>
</ScreenContainer>
);
}
function VerdictCard({ v }: { v: Verdict }) {
if (!v.available) {
return (
<Card padding="md" style={{ marginTop: 12, backgroundColor: colors.surface }}>
<Text style={{ ...typography.caption, color: colors.textSecondary } as any}>{v.caution}</Text>
</Card>
);
}
return (
<Card padding="md" style={{ marginTop: 12, backgroundColor: colors.surface }}>
<Badge label="청구 가능" tone="success" />
<View style={{ marginTop: 10 }}>
<Text style={styles.section}>💰 </Text>
{v.policies.map((p, i) => (
<Text key={i} style={styles.item}>
{p.name} {p.desc}
</Text>
))}
</View>
<View style={{ marginTop: 10 }}>
<Text style={styles.section}>📋 </Text>
{v.docs.map((d, i) => (
<Text key={i} style={styles.item}>
{d}
</Text>
))}
</View>
<View style={{ marginTop: 10 }}>
<Text style={styles.section}>💵 </Text>
<Text style={{ ...typography.title as any, color: colors.success, marginTop: 4 }}>{v.estimated}</Text>
</View>
{v.caution && (
<View style={{ marginTop: 10, padding: 10, backgroundColor: colors.warningLight, borderRadius: 8 }}>
<Text style={{ ...typography.small, color: colors.warning } as any}> {v.caution}</Text>
</View>
)}
</Card>
);
}
const styles = StyleSheet.create({
hero: { padding: 24, borderRadius: radius.xl, alignItems: 'center' },
heroTitle: { color: '#FFF', fontSize: 20, fontWeight: '800', marginTop: 10 },
heroSub: { color: 'rgba(255,255,255,0.9)', fontSize: 13, marginTop: 4 },
msg: {
padding: 14,
borderRadius: 16,
marginBottom: 8,
maxWidth: '88%',
},
msgAI: { backgroundColor: colors.surfaceAlt, alignSelf: 'flex-start' },
msgUser: { backgroundColor: colors.primary, alignSelf: 'flex-end' },
sample: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primaryLight,
padding: 12,
borderRadius: radius.md,
},
inputRow: { flexDirection: 'row', alignItems: 'flex-end' },
input: {
flex: 1,
fontSize: 15,
maxHeight: 80,
color: colors.text,
paddingVertical: 8,
},
sendBtn: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 8,
},
section: { ...typography.caption, color: colors.textSecondary, fontWeight: '700' },
item: { ...typography.body, marginTop: 4, color: colors.text },
disclaimer: { ...typography.small, color: colors.textTertiary, marginTop: 8, textAlign: 'center' },
});
+134
View File
@@ -0,0 +1,134 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import { BarChart } from 'react-native-chart-kit';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Badge from '@/components/Badge';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
const screenWidth = Dimensions.get('window').width;
const data = {
'20대': { prem: 18, must: ['실손', '상해'], rec: ['암 (가족력)'], avgCoverage: '2,000만원' },
'30대': { prem: 28, must: ['실손', '암', '종신'], rec: ['여성특화', '치아'], avgCoverage: '5,000만원' },
'40대': { prem: 38, must: ['실손', '암', '뇌/심장', '종신'], rec: ['간병', '치매'], avgCoverage: '1억원' },
'50대': { prem: 52, must: ['실손', '암', '뇌/심장', '간병'], rec: ['치매', '유병자'], avgCoverage: '1.5억원' },
'60대+': { prem: 48, must: ['유병자 실손', '간병', '치매'], rec: ['상조'], avgCoverage: '1억원' },
} as const;
const ages = ['20대', '30대', '40대', '50대', '60대+'] as const;
const genders = ['여성', '남성'] as const;
export default function AnalysisScreen() {
const [age, setAge] = useState<(typeof ages)[number]>('30대');
const [gender, setGender] = useState<(typeof genders)[number]>('여성');
const info = data[age];
return (
<ScreenContainer>
<Header title="연령별 분석" />
<View style={{ padding: spacing.lg }}>
<Card>
<Text style={typography.bodyBold as any}> </Text>
<View style={{ flexDirection: 'row', gap: 6, marginTop: 12, flexWrap: 'wrap' }}>
{ages.map((a) => (
<TouchableOpacity key={a} style={[styles.pill, age === a && styles.pillActive]} onPress={() => setAge(a)}>
<Text style={[styles.pillText, age === a && styles.pillTextActive]}>{a}</Text>
</TouchableOpacity>
))}
</View>
<View style={{ flexDirection: 'row', gap: 6, marginTop: 8 }}>
{genders.map((g) => (
<TouchableOpacity key={g} style={[styles.pill, gender === g && styles.pillActive]} onPress={() => setGender(g)}>
<Text style={[styles.pillText, gender === g && styles.pillTextActive]}>{g}</Text>
</TouchableOpacity>
))}
</View>
</Card>
<Card style={{ marginTop: 12 }}>
<Text style={typography.caption as any}>📊 {age} {gender}?</Text>
<Text style={[typography.h1 as any, { color: colors.primary, marginTop: 6 }]}>
{info.prem}
</Text>
<Text style={{ ...typography.caption, color: colors.textSecondary } as any}>
{info.avgCoverage}
</Text>
<View style={{ marginTop: 16 }}>
<Text style={styles.sub}> </Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginTop: 6 }}>
{info.must.map((m) => (
<Badge key={m} label={m} tone="primary" />
))}
</View>
</View>
<View style={{ marginTop: 12 }}>
<Text style={styles.sub}> </Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginTop: 6 }}>
{info.rec.map((m) => (
<Badge key={m} label={m} tone="success" />
))}
</View>
</View>
</Card>
</View>
<Section title="연령별 평균 월 보험료">
<Card padding="md">
<BarChart
data={{
labels: ages as unknown as string[],
datasets: [{ data: ages.map((a) => data[a].prem) }],
}}
width={screenWidth - spacing.lg * 2 - spacing.md * 2}
height={220}
yAxisLabel=""
yAxisSuffix="만"
fromZero
chartConfig={{
backgroundGradientFrom: '#FFF',
backgroundGradientTo: '#FFF',
decimalPlaces: 0,
color: (o = 1) => `rgba(59, 130, 246, ${o})`,
labelColor: (o = 1) => `rgba(107, 114, 128, ${o})`,
propsForBackgroundLines: { stroke: colors.border, strokeDasharray: '3,3' },
barPercentage: 0.6,
}}
style={{ borderRadius: radius.md, marginLeft: -10 }}
/>
</Card>
</Section>
<Section title="💡 이런 점을 확인해 보세요">
{[
'내 월 보험료가 평균보다 30% 이상 높다면 다이어트 진단 필요',
'평균보다 보장금액이 낮다면 보장 공백이 있을 수 있음',
'필수 보험이 미가입이라면 우선순위로 점검',
].map((tip, i) => (
<Card key={i} style={{ marginBottom: 8 }}>
<Text style={typography.body as any}> {tip}</Text>
</Card>
))}
</Section>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
pill: {
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: radius.pill,
backgroundColor: colors.surfaceAlt,
borderWidth: 1,
borderColor: colors.border,
},
pillActive: { backgroundColor: colors.primary, borderColor: colors.primary },
pillText: { color: colors.text, fontSize: 13, fontWeight: '500' },
pillTextActive: { color: '#FFF', fontWeight: '700' },
sub: { ...typography.caption, color: colors.textSecondary, fontWeight: '600' },
});
+101
View File
@@ -0,0 +1,101 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Badge from '@/components/Badge';
import Button from '@/components/Button';
import { useAppStore } from '@/store/useAppStore';
import { colors } from '@/theme/colors';
import { spacing, typography } from '@/theme/typography';
import type { RootStackParamList } from '@/navigation/RootNavigator';
type Nav = NativeStackNavigationProp<RootStackParamList>;
export default function ClaimHubScreen() {
const nav = useNavigation<Nav>();
const claims = useAppStore((s) => s.claims);
return (
<ScreenContainer>
<Header title="보험금" showBack={false} />
<View style={{ padding: spacing.lg }}>
<Card>
<Text style={styles.h1}>📸 , </Text>
<Text style={styles.dim}> </Text>
<View style={{ marginTop: 16, gap: 8 }}>
<Button title="보험금 청구하기" onPress={() => nav.navigate('Claim')} />
<Button title="AI 보험금 판정 받기" variant="outline" onPress={() => nav.navigate('AIJudge')} />
</View>
</Card>
</View>
<Section title="진행 상태">
{claims.map((c) => (
<Card key={c.id} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row' }}>
<View style={{ flex: 1 }}>
<Badge
label={c.status}
tone={
c.status === '지급완료' ? 'success' : c.status === '심사' ? 'primary' : c.status === '서류보완' ? 'warning' : 'neutral'
}
/>
<Text style={[typography.bodyBold as any, { marginTop: 6 }]}>{c.title}</Text>
<Text style={styles.dim}>{c.date}</Text>
</View>
{c.amount ? <Text style={styles.amt}>{c.amount.toLocaleString()}</Text> : null}
</View>
</Card>
))}
</Section>
<Section title="보험금 관련 바로가기">
<View style={{ gap: 8 }}>
<Card onPress={() => nav.navigate('HiddenMoney')}>
<View style={styles.row}>
<Ionicons name="cash" size={22} color={colors.success} />
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={typography.bodyBold as any}> </Text>
<Text style={styles.dim}> </Text>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.textTertiary} />
</View>
</Card>
<Card onPress={() => nav.navigate('DiseaseCode')}>
<View style={styles.row}>
<Ionicons name="search" size={22} color={colors.accent} />
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={typography.bodyBold as any}> </Text>
<Text style={styles.dim}>"내 질병이 보장되나요?"</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.textTertiary} />
</View>
</Card>
<Card onPress={() => nav.navigate('HospitalChecklist')}>
<View style={styles.row}>
<Ionicons name="medkit" size={22} color="#14B8A6" />
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={typography.bodyBold as any}> </Text>
<Text style={styles.dim}> !</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.textTertiary} />
</View>
</Card>
</View>
</Section>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
h1: { ...typography.h3, color: colors.text },
dim: { ...typography.caption, color: colors.textSecondary, marginTop: 4 },
amt: { ...typography.title, color: colors.text, alignSelf: 'center' },
row: { flexDirection: 'row', alignItems: 'center' },
});
+197
View File
@@ -0,0 +1,197 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Image, Alert } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import Badge from '@/components/Badge';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
type DocType = 'receipt' | 'diagnosis' | 'detail';
const docLabels: Record<DocType, { title: string; desc: string; icon: keyof typeof Ionicons.glyphMap }> = {
receipt: { title: '영수증', desc: '진료비 영수증 원본', icon: 'receipt' },
diagnosis: { title: '진단서', desc: '병원 발급 진단서', icon: 'document-text' },
detail: { title: '세부내역서', desc: '비급여 항목 포함', icon: 'list' },
};
const steps = [
{ step: 1, label: '서류 준비', desc: '영수증/진단서 촬영' },
{ step: 2, label: '정보 입력', desc: '병원명·진료일자' },
{ step: 3, label: '자동 전송', desc: '보험사로 즉시 전달' },
{ step: 4, label: '진행 상태', desc: '실시간 확인' },
];
export default function ClaimScreen() {
const [docs, setDocs] = useState<Record<DocType, string | null>>({
receipt: null,
diagnosis: null,
detail: null,
});
const [hospital, setHospital] = useState('');
const [visitDate, setVisitDate] = useState('');
const pick = async (type: DocType) => {
const res = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.7,
});
if (!res.canceled && res.assets[0]) {
setDocs({ ...docs, [type]: res.assets[0].uri });
}
};
const capture = async (type: DocType) => {
const perm = await ImagePicker.requestCameraPermissionsAsync();
if (!perm.granted) {
Alert.alert('카메라 권한이 필요합니다.');
return;
}
const res = await ImagePicker.launchCameraAsync({ quality: 0.7 });
if (!res.canceled && res.assets[0]) {
setDocs({ ...docs, [type]: res.assets[0].uri });
}
};
const submit = () => {
if (!docs.receipt) {
Alert.alert('영수증은 필수입니다.');
return;
}
Alert.alert('청구 접수 완료', '보험사에서 심사 후 3~7영업일 내 지급됩니다.');
};
return (
<ScreenContainer>
<Header title="보험금 청구" />
<View style={{ padding: spacing.lg }}>
<Card>
<Text style={typography.bodyBold as any}>📸 </Text>
<View style={{ marginTop: 12, gap: 10 }}>
{steps.map((s) => (
<View key={s.step} style={styles.stepRow}>
<View style={styles.stepCircle}>
<Text style={styles.stepNum}>{s.step}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={typography.bodyBold as any}>{s.label}</Text>
<Text style={{ ...typography.caption, color: colors.textSecondary } as any}>{s.desc}</Text>
</View>
</View>
))}
</View>
</Card>
<Section title="서류 업로드">
{(Object.keys(docLabels) as DocType[]).map((t) => {
const info = docLabels[t];
const uri = docs[t];
return (
<Card key={t} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={styles.docIcon}>
<Ionicons name={info.icon} size={22} color={colors.primary} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={typography.bodyBold as any}>{info.title}</Text>
{t === 'receipt' && <Badge label="필수" tone="danger" style={{ marginLeft: 6 }} />}
</View>
<Text style={{ ...typography.caption, color: colors.textSecondary } as any}>{info.desc}</Text>
</View>
{uri ? (
<TouchableOpacity onPress={() => setDocs({ ...docs, [t]: null })}>
<Image source={{ uri }} style={styles.thumb} />
</TouchableOpacity>
) : (
<View style={{ flexDirection: 'row', gap: 6 }}>
<TouchableOpacity style={styles.smallBtn} onPress={() => capture(t)}>
<Ionicons name="camera" size={18} color={colors.primary} />
</TouchableOpacity>
<TouchableOpacity style={styles.smallBtn} onPress={() => pick(t)}>
<Ionicons name="image" size={18} color={colors.primary} />
</TouchableOpacity>
</View>
)}
</View>
</Card>
);
})}
</Section>
<Section title="청구 정보">
<Card>
<Text style={styles.label}></Text>
<Text style={styles.inputView}>{hospital || '병원명 입력'}</Text>
<View style={{ flexDirection: 'row', gap: 6, flexWrap: 'wrap', marginTop: 4 }}>
{['강남세브란스', '서울아산병원', '삼성의료원', '고려대병원'].map((h) => (
<TouchableOpacity key={h} style={styles.pill} onPress={() => setHospital(h)}>
<Text style={styles.pillText}>{h}</Text>
</TouchableOpacity>
))}
</View>
<Text style={[styles.label, { marginTop: 12 }]}></Text>
<Text style={styles.inputView}>{visitDate || '2026-04-22'}</Text>
</Card>
</Section>
<View style={{ paddingTop: 16, gap: 8 }}>
<Button title="보험금 청구하기" size="lg" onPress={submit} />
<Button title="AI 판정으로 예상 금액 확인" variant="outline" onPress={() => {}} />
</View>
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
stepRow: { flexDirection: 'row', alignItems: 'center' },
stepCircle: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
stepNum: { color: '#FFF', fontWeight: '700' },
docIcon: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.primaryLight,
alignItems: 'center',
justifyContent: 'center',
},
thumb: { width: 50, height: 50, borderRadius: 8 },
smallBtn: {
width: 38,
height: 38,
borderRadius: 19,
borderWidth: 1,
borderColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
},
label: { ...typography.caption, color: colors.textSecondary, fontWeight: '600' },
inputView: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: radius.md,
padding: 14,
marginTop: 6,
color: colors.text,
},
pill: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: radius.pill,
backgroundColor: colors.surfaceAlt,
},
pillText: { ...typography.small, color: colors.text },
});
+96
View File
@@ -0,0 +1,96 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import { colors } from '@/theme/colors';
import { spacing, typography } from '@/theme/typography';
import type { RootStackParamList } from '@/navigation/RootNavigator';
type Nav = NativeStackNavigationProp<RootStackParamList>;
const planners = [
{ name: '김보험', title: '수석 설계사', career: '경력 15년', tag: '실손 전문', rating: 4.9 },
{ name: '이담당', title: '전문 설계사', career: '경력 8년', tag: '암보험 전문', rating: 4.8 },
{ name: '최매니저', title: '여성보험 전문', career: '경력 12년', tag: '여성보험', rating: 4.9 },
];
export default function ConsultHubScreen() {
const nav = useNavigation<Nav>();
return (
<ScreenContainer>
<Header title="상담" showBack={false} />
<View style={{ padding: spacing.lg }}>
<Card>
<Text style={styles.h2}>💬 1:1 </Text>
<Text style={styles.dim}> </Text>
<View style={{ marginTop: 16, gap: 8 }}>
<Button title="카카오톡 상담" variant="kakao" leftIcon={<Ionicons name="chatbubble" size={16} color="#191600" />} onPress={() => nav.navigate('Consult')} />
<Button title="전화 상담 예약" variant="primary" leftIcon={<Ionicons name="call" size={16} color="#FFF" />} onPress={() => nav.navigate('Consult')} />
<Button title="방문 상담 신청" variant="outline" onPress={() => nav.navigate('Consult')} />
</View>
</Card>
</View>
<Section title="추천 설계사">
{planners.map((p) => (
<Card key={p.name} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={styles.avatar}>
<Ionicons name="person" size={28} color={colors.primary} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={typography.title as any}>{p.name}</Text>
<Text style={[styles.dim, { marginLeft: 6 }]}>{p.title}</Text>
</View>
<Text style={styles.dim}>
{p.career} · {p.tag}
</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 4 }}>
<Ionicons name="star" size={14} color="#F59E0B" />
<Text style={[styles.dim, { marginLeft: 4 }]}>{p.rating}</Text>
</View>
</View>
<Button title="상담" size="sm" onPress={() => nav.navigate('Consult')} />
</View>
</Card>
))}
</Section>
<Section title="AI 상담 도우미">
<Card onPress={() => nav.navigate('AIJudge')}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={[styles.avatar, { backgroundColor: '#EDE9FE' }]}>
<Ionicons name="sparkles" size={24} color="#8B5CF6" />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={typography.bodyBold as any}>AI </Text>
<Text style={styles.dim}> </Text>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.textTertiary} />
</View>
</Card>
</Section>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
h2: { ...typography.h3, color: colors.text },
dim: { ...typography.caption, color: colors.textSecondary, marginTop: 4 },
avatar: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: colors.primaryLight,
alignItems: 'center',
justifyContent: 'center',
},
});
+157
View File
@@ -0,0 +1,157 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, TextInput, TouchableOpacity, Linking, Alert } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
const times = ['오전 10시', '오전 11시', '오후 2시', '오후 3시', '오후 4시', '오후 5시'];
export default function ConsultScreen() {
const [method, setMethod] = useState<'kakao' | 'phone' | 'visit'>('kakao');
const [phone, setPhone] = useState('');
const [date, setDate] = useState('');
const [time, setTime] = useState('');
const [memo, setMemo] = useState('');
const submit = () => {
const label = method === 'kakao' ? '카카오톡' : method === 'phone' ? '전화' : '방문';
Alert.alert(`${label} 상담 접수 완료`, '전문 설계사가 곧 연락드립니다.');
};
return (
<ScreenContainer>
<Header title="전문가 상담" />
<View style={{ padding: spacing.lg }}>
<Card>
<Text style={typography.bodyBold as any}> </Text>
<View style={{ flexDirection: 'row', gap: 8, marginTop: 12 }}>
{(
[
{ k: 'kakao', label: '카카오톡', icon: 'chatbubble' },
{ k: 'phone', label: '전화', icon: 'call' },
{ k: 'visit', label: '방문', icon: 'home' },
] as const
).map((m) => (
<TouchableOpacity
key={m.k}
style={[styles.methodBox, method === m.k && styles.methodBoxActive]}
onPress={() => setMethod(m.k)}
>
<Ionicons name={m.icon as any} size={24} color={method === m.k ? '#FFF' : colors.text} />
<Text style={[styles.methodText, method === m.k && { color: '#FFF' }]}>{m.label}</Text>
</TouchableOpacity>
))}
</View>
</Card>
{method !== 'kakao' && (
<Card style={{ marginTop: 12 }}>
<Text style={typography.bodyBold as any}> </Text>
<Text style={[styles.label, { marginTop: 12 }]}></Text>
<TextInput
style={styles.input}
placeholder="010-0000-0000"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
/>
<Text style={[styles.label, { marginTop: 12 }]}> </Text>
<TextInput style={styles.input} placeholder="YYYY-MM-DD" value={date} onChangeText={setDate} />
<Text style={[styles.label, { marginTop: 12 }]}> </Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginTop: 4 }}>
{times.map((t) => (
<TouchableOpacity key={t} style={[styles.pill, time === t && styles.pillActive]} onPress={() => setTime(t)}>
<Text style={[styles.pillText, time === t && styles.pillTextActive]}>{t}</Text>
</TouchableOpacity>
))}
</View>
<Text style={[styles.label, { marginTop: 12 }]}> </Text>
<TextInput
style={[styles.input, { minHeight: 80, textAlignVertical: 'top' }]}
placeholder="상담받고 싶은 내용을 적어주세요"
multiline
value={memo}
onChangeText={setMemo}
/>
</Card>
)}
{method === 'kakao' && (
<Card style={{ marginTop: 12 }}>
<Text style={typography.bodyBold as any}> </Text>
<Text style={{ ...typography.caption, color: colors.textSecondary, marginTop: 6 } as any}>
'보험케어' .
</Text>
<View style={{ marginTop: 12 }}>
<Button
title="카카오톡 채널 열기"
variant="kakao"
leftIcon={<Ionicons name="chatbubble" size={16} color="#191600" />}
onPress={() =>
Linking.openURL('https://pf.kakao.com/').catch(() => Alert.alert('카카오톡이 설치되지 않았습니다.'))
}
/>
</View>
</Card>
)}
<Section title="상담 시 유의사항">
<Card padding="md">
{[
'본인이 가입한 보험 증권을 준비해 주세요',
'개인정보는 안전하게 암호화되어 전달됩니다',
'상담은 가입 권유가 아닌 점검 목적이 원칙입니다',
].map((t, i) => (
<Text key={i} style={[typography.body as any, { marginVertical: 4 }]}>
{t}
</Text>
))}
</Card>
</Section>
<View style={{ paddingTop: 16 }}>
<Button title={method === 'kakao' ? '카카오톡으로 상담하기' : '상담 예약하기'} size="lg" onPress={submit} />
</View>
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
methodBox: {
flex: 1,
paddingVertical: 18,
borderRadius: radius.md,
borderWidth: 1.5,
borderColor: colors.border,
alignItems: 'center',
backgroundColor: colors.surface,
},
methodBoxActive: { backgroundColor: colors.primary, borderColor: colors.primary },
methodText: { marginTop: 6, fontSize: 13, fontWeight: '600', color: colors.text },
label: { ...typography.caption, color: colors.textSecondary, fontWeight: '600' },
input: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: radius.md,
padding: 12,
fontSize: 14,
marginTop: 6,
},
pill: {
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: radius.pill,
backgroundColor: colors.surfaceAlt,
borderWidth: 1,
borderColor: colors.border,
},
pillActive: { backgroundColor: colors.primary, borderColor: colors.primary },
pillText: { color: colors.text, fontSize: 13 },
pillTextActive: { color: '#FFF', fontWeight: '700' },
});
+247
View File
@@ -0,0 +1,247 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, TextInput, TouchableOpacity } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import Badge from '@/components/Badge';
import ProgressBar from '@/components/ProgressBar';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
import type { RootStackParamList } from '@/navigation/RootNavigator';
type Nav = NativeStackNavigationProp<RootStackParamList>;
type Step = 'intro' | 'basic' | 'questions' | 'result';
const questions = [
{ key: 'smoke', q: '흡연을 하시나요?', options: ['비흡연', '흡연'] },
{ key: 'hospital', q: '최근 1년간 병원 입원 경험?', options: ['없음', '1~2회', '3회 이상'] },
{ key: 'family', q: '가족력(암/뇌/심장)이 있나요?', options: ['없음', '있음'] },
{ key: 'exercise', q: '주 운동 횟수는?', options: ['거의 안함', '주 1~2회', '주 3회 이상'] },
{ key: 'current', q: '현재 가입된 보험 개수?', options: ['없음', '1~2개', '3개 이상'] },
{ key: 'budget', q: '월 보험료 희망 예산?', options: ['10만원 이하', '10~30만원', '30만원 이상'] },
];
export default function DiagnosisScreen() {
const nav = useNavigation<Nav>();
const [step, setStep] = useState<Step>('intro');
const [age, setAge] = useState('34');
const [gender, setGender] = useState<'남' | '여'>('남');
const [job, setJob] = useState('사무직');
const [qIdx, setQIdx] = useState(0);
const [answers, setAnswers] = useState<Record<string, string>>({});
const progress = step === 'intro' ? 0 : step === 'basic' ? 25 : step === 'questions' ? 25 + (qIdx / questions.length) * 50 : 100;
if (step === 'intro') {
return (
<ScreenContainer>
<Header title="3초 보험 진단" />
<View style={{ padding: spacing.lg }}>
<LinearGradient colors={colors.gradient.primary} style={styles.intro}>
<Ionicons name="flash" size={48} color="#FFF" />
<Text style={styles.introTitle}> , 3 OK</Text>
<Text style={styles.introSub}>
{'\n'}AI가
</Text>
</LinearGradient>
<Card style={{ marginTop: 16 }}>
<Text style={typography.bodyBold as any}> </Text>
<View style={{ marginTop: 12, gap: 10 }}>
{['기본 정보 입력 (나이/성별/직업)', '5~10개 맞춤 질문 응답', 'AI 분석 (3초)', '맞춤 보험 추천 & 점수 확인'].map(
(s, i) => (
<View key={i} style={styles.stepRow}>
<View style={styles.stepCircle}>
<Text style={styles.stepNum}>{i + 1}</Text>
</View>
<Text style={{ flex: 1, ...typography.body, color: colors.text } as any}>{s}</Text>
</View>
)
)}
</View>
</Card>
<View style={{ marginTop: 20 }}>
<Button title="진단 시작하기" size="lg" onPress={() => setStep('basic')} />
</View>
</View>
</ScreenContainer>
);
}
if (step === 'basic') {
return (
<ScreenContainer>
<Header title="기본 정보" subtitle="1/3 단계" />
<View style={{ padding: spacing.lg }}>
<ProgressBar value={progress} />
<Card style={{ marginTop: 16 }}>
<Text style={styles.label}></Text>
<TextInput style={styles.input} value={age} onChangeText={setAge} keyboardType="number-pad" />
<Text style={[styles.label, { marginTop: 16 }]}></Text>
<View style={{ flexDirection: 'row', gap: 8 }}>
{(['남', '여'] as const).map((g) => (
<TouchableOpacity
key={g}
style={[styles.pill, gender === g && styles.pillActive]}
onPress={() => setGender(g)}
>
<Text style={[styles.pillText, gender === g && styles.pillTextActive]}>{g}</Text>
</TouchableOpacity>
))}
</View>
<Text style={[styles.label, { marginTop: 16 }]}></Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
{['사무직', '생산직', '전문직', '학생', '주부', '기타'].map((j) => (
<TouchableOpacity key={j} style={[styles.pill, job === j && styles.pillActive]} onPress={() => setJob(j)}>
<Text style={[styles.pillText, job === j && styles.pillTextActive]}>{j}</Text>
</TouchableOpacity>
))}
</View>
</Card>
<View style={{ marginTop: 20 }}>
<Button title="다음" size="lg" onPress={() => setStep('questions')} />
</View>
</View>
</ScreenContainer>
);
}
if (step === 'questions') {
const q = questions[qIdx];
return (
<ScreenContainer>
<Header title="맞춤 질문" subtitle={`${qIdx + 1}/${questions.length}`} />
<View style={{ padding: spacing.lg }}>
<ProgressBar value={progress} />
<Card style={{ marginTop: 16 }}>
<Badge label={`질문 ${qIdx + 1}`} tone="primary" />
<Text style={[typography.h3 as any, { marginTop: 10 }]}>{q.q}</Text>
<View style={{ marginTop: 16, gap: 10 }}>
{q.options.map((opt) => (
<TouchableOpacity
key={opt}
style={styles.optionBox}
onPress={() => {
setAnswers({ ...answers, [q.key]: opt });
if (qIdx < questions.length - 1) setQIdx(qIdx + 1);
else setStep('result');
}}
>
<Text style={styles.optionText}>{opt}</Text>
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
</TouchableOpacity>
))}
</View>
</Card>
{qIdx > 0 && (
<View style={{ marginTop: 16 }}>
<Button title="이전" variant="outline" onPress={() => setQIdx(qIdx - 1)} />
</View>
)}
</View>
</ScreenContainer>
);
}
// result
return (
<ScreenContainer>
<Header title="진단 결과" />
<View style={{ padding: spacing.lg }}>
<LinearGradient colors={colors.gradient.success} style={styles.resultHero}>
<Ionicons name="checkmark-circle" size={48} color="#FFF" />
<Text style={styles.resultTitle}> !</Text>
<Text style={styles.resultSub}>
{age} {gender} {job}
</Text>
</LinearGradient>
<Section title="🎯 추천 보험">
{[
{ name: '4세대 실손의료비', reason: '기본 의료비 보장 필수', priority: '필수' },
{ name: '종합암보험', reason: '가족력 있어 진단비 강화 필요', priority: '필수' },
{ name: '상해보험', reason: '운전/활동량 기반 추천', priority: '권장' },
{ name: '치아보험', reason: '30대 관리 시점', priority: '선택' },
].map((r) => (
<Card key={r.name} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ flex: 1 }}>
<Badge
label={r.priority}
tone={r.priority === '필수' ? 'danger' : r.priority === '권장' ? 'warning' : 'primary'}
/>
<Text style={[typography.bodyBold as any, { marginTop: 6 }]}>{r.name}</Text>
<Text style={styles.dim}>{r.reason}</Text>
</View>
</View>
</Card>
))}
</Section>
<View style={{ padding: spacing.lg, gap: 8 }}>
<Button title="내 점수 상세보기" onPress={() => nav.replace('Score')} />
<Button title="전문가 상담 받기" variant="outline" onPress={() => nav.replace('Consult')} />
</View>
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
intro: { padding: 24, borderRadius: radius.xl, alignItems: 'center' },
introTitle: { color: '#FFF', fontSize: 22, fontWeight: '800', marginTop: 14 },
introSub: { color: 'rgba(255,255,255,0.9)', fontSize: 14, textAlign: 'center', marginTop: 8, lineHeight: 20 },
stepRow: { flexDirection: 'row', alignItems: 'center' },
stepCircle: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.primaryLight,
alignItems: 'center',
justifyContent: 'center',
marginRight: 10,
},
stepNum: { color: colors.primary, fontWeight: '700', fontSize: 13 },
label: { ...typography.caption, color: colors.textSecondary, marginBottom: 8, fontWeight: '600' },
input: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: radius.md,
padding: 14,
fontSize: 16,
color: colors.text,
},
pill: {
paddingHorizontal: 18,
paddingVertical: 10,
borderRadius: radius.pill,
backgroundColor: colors.surfaceAlt,
borderWidth: 1,
borderColor: colors.border,
},
pillActive: { backgroundColor: colors.primary, borderColor: colors.primary },
pillText: { color: colors.text, fontWeight: '500' },
pillTextActive: { color: '#FFF', fontWeight: '700' },
optionBox: {
padding: 16,
borderRadius: radius.md,
borderWidth: 1.5,
borderColor: colors.border,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
optionText: { ...typography.body, color: colors.text, fontWeight: '600' },
resultHero: { padding: 24, borderRadius: radius.xl, alignItems: 'center' },
resultTitle: { color: '#FFF', fontSize: 24, fontWeight: '800', marginTop: 10 },
resultSub: { color: 'rgba(255,255,255,0.9)', marginTop: 4 },
dim: { ...typography.caption, color: colors.textSecondary, marginTop: 4 },
});
+106
View File
@@ -0,0 +1,106 @@
import React, { useState, useMemo } from 'react';
import { View, Text, StyleSheet, TextInput } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Badge from '@/components/Badge';
import { searchDisease, diseaseDB } from '@/data/diseaseCodes';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
export default function DiseaseCodeScreen() {
const [query, setQuery] = useState('');
const results = useMemo(() => (query ? searchDisease(query) : diseaseDB.slice(0, 4)), [query]);
return (
<ScreenContainer>
<Header title="질병코드 조회" />
<View style={{ padding: spacing.lg }}>
<Card>
<Text style={typography.bodyBold as any}>🏥 </Text>
<Text style={{ ...typography.caption, color: colors.textSecondary, marginTop: 4 } as any}>
) "갑상선 결절", "발목 삐었을 때", "유방암"
</Text>
<View style={styles.searchBox}>
<Ionicons name="search" size={18} color={colors.textSecondary} />
<TextInput
style={styles.search}
value={query}
onChangeText={setQuery}
placeholder="질병명을 입력하세요"
placeholderTextColor={colors.textTertiary}
returnKeyType="search"
/>
{query.length > 0 && (
<Ionicons name="close-circle" size={18} color={colors.textTertiary} onPress={() => setQuery('')} />
)}
</View>
</Card>
<Section title={query ? `"${query}" 검색 결과 (${results.length}건)` : '자주 찾는 질병'}>
{results.length === 0 && (
<Card>
<Text style={typography.body as any}> . .</Text>
</Card>
)}
{results.map((d) => (
<Card key={d.code} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 8 }}>
<Badge label={d.code} tone="neutral" />
<Badge label={d.category} tone="primary" style={{ marginLeft: 6 }} />
</View>
<Text style={typography.h3 as any}>{d.name}</Text>
<View style={styles.sep} />
<Text style={styles.label}>💰 </Text>
<View style={{ gap: 6, marginTop: 6 }}>
{d.coverage.map((c, i) => (
<View key={i} style={styles.coverRow}>
<Ionicons name="shield-checkmark" size={16} color={colors.primary} />
<View style={{ flex: 1, marginLeft: 8 }}>
<Text style={{ ...typography.body as any, fontWeight: '600' }}>{c.policy}</Text>
<Text style={styles.cov}>{c.amount}</Text>
{c.note && <Text style={styles.note}> {c.note}</Text>}
</View>
</View>
))}
</View>
{d.tips && d.tips.length > 0 && (
<>
<View style={styles.sep} />
<Text style={styles.label}>📌 TIP</Text>
{d.tips.map((t, i) => (
<Text key={i} style={[styles.tip]}>
{t}
</Text>
))}
</>
)}
</Card>
))}
</Section>
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
searchBox: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.border,
borderRadius: radius.md,
paddingHorizontal: 12,
paddingVertical: 6,
marginTop: 12,
},
search: { flex: 1, marginLeft: 8, fontSize: 15, paddingVertical: 8 },
label: { ...typography.caption, color: colors.textSecondary, fontWeight: '700' },
sep: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
coverRow: { flexDirection: 'row', alignItems: 'flex-start' },
cov: { ...typography.caption, color: colors.primary, fontWeight: '700', marginTop: 2 },
note: { ...typography.small, color: colors.warning, marginTop: 2 },
tip: { ...typography.body, color: colors.text, marginTop: 4 },
});
+139
View File
@@ -0,0 +1,139 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Badge from '@/components/Badge';
import ProgressBar from '@/components/ProgressBar';
import Button from '@/components/Button';
import { useAppStore, FamilyMember } from '@/store/useAppStore';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
const relationIcon: Record<FamilyMember['relation'], keyof typeof Ionicons.glyphMap> = {
: 'person',
: 'heart',
: 'happy',
: 'people',
: 'people-outline',
};
const essentialMap: Record<string, string[]> = {
: ['실손', '암', '상해'],
: ['실손', '암', '여성'],
: ['어린이'],
: ['실손', '간병'],
};
export default function FamilyScreen() {
const family = useAppStore((s) => s.family);
const analyze = (m: FamilyMember) => {
const required = essentialMap[m.relation] ?? [];
const have = new Set(m.policies.map((p) => p.type));
const missing = required.filter((r) => !have.has(r as any));
const covered = required.length - missing.length;
const score = required.length === 0 ? 100 : Math.round((covered / required.length) * 100);
return { required, missing, covered, score };
};
const avgScore = Math.round(family.reduce((a, m) => a + analyze(m).score, 0) / family.length);
return (
<ScreenContainer>
<Header title="가족 보험" />
<View style={{ padding: spacing.lg }}>
<Card>
<Text style={typography.caption as any}>👨👩👧 </Text>
<Text style={{ ...typography.h1 as any, color: colors.primary, marginTop: 6 }}>{avgScore}</Text>
<View style={{ height: 10 }} />
<ProgressBar value={avgScore} />
<Text style={{ ...typography.caption, color: colors.textSecondary, marginTop: 8 } as any}>
{family.length}
</Text>
</Card>
<Section title="가족 구성원">
{family.map((m) => {
const a = analyze(m);
return (
<Card key={m.id} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={styles.avatar}>
<Ionicons name={relationIcon[m.relation]} size={26} color={colors.primary} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={typography.bodyBold as any}>{m.name}</Text>
<Badge label={m.relation} tone="primary" style={{ marginLeft: 6 }} />
</View>
<Text style={{ ...typography.caption, color: colors.textSecondary } as any}>
{m.age} · {m.gender}
</Text>
</View>
<Text style={{ ...typography.h3 as any, color: scoreColor(a.score) }}>{a.score}</Text>
</View>
<View style={styles.sep} />
<View style={{ gap: 6 }}>
{m.policies.map((p) => (
<View key={p.id} style={styles.policyRow}>
<Ionicons name="checkmark-circle" size={16} color={colors.success} />
<Text style={{ ...typography.body, marginLeft: 6, flex: 1 } as any} numberOfLines={1}>
{p.type} · {p.name}
</Text>
<Text style={{ ...typography.small, color: colors.textSecondary } as any}>
{(p.coverage / 10000).toLocaleString()}
</Text>
</View>
))}
{a.missing.map((miss) => (
<View key={miss} style={styles.policyRow}>
<Ionicons name="close-circle" size={16} color={colors.danger} />
<Text style={{ ...typography.body, marginLeft: 6, color: colors.danger, flex: 1 } as any}>
{miss}
</Text>
</View>
))}
</View>
</Card>
);
})}
</Section>
<Section title="💡 가족 보험 TIP">
<Card>
<Text style={typography.body as any}>
{'\n'}
65 / {'\n'}
</Text>
</Card>
</Section>
<View style={{ paddingTop: 16 }}>
<Button title="가족 구성원 추가" variant="outline" />
</View>
</View>
</ScreenContainer>
);
}
function scoreColor(s: number) {
return s >= 80 ? colors.success : s >= 60 ? colors.warning : colors.danger;
}
const styles = StyleSheet.create({
avatar: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: colors.primaryLight,
alignItems: 'center',
justifyContent: 'center',
},
sep: { height: 1, backgroundColor: colors.border, marginVertical: 12 },
policyRow: { flexDirection: 'row', alignItems: 'center' },
});
+127
View File
@@ -0,0 +1,127 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, TextInput, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import Badge from '@/components/Badge';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
type Metric = { key: string; label: string; value: string; normal: string; status: 'good' | 'warn' | 'bad' };
export default function HealthCheckScreen() {
const [metrics, setMetrics] = useState<Metric[]>([
{ key: 'bp', label: '혈압', value: '145/92', normal: '120/80 이하', status: 'bad' },
{ key: 'chol', label: '총콜레스테롤', value: '245', normal: '200 이하', status: 'warn' },
{ key: 'bs', label: '공복혈당', value: '102', normal: '100 이하', status: 'warn' },
{ key: 'bmi', label: 'BMI', value: '27.3', normal: '23 이하', status: 'warn' },
{ key: 'polyp', label: '대장 용종', value: '발견', normal: '없음', status: 'bad' },
{ key: 'liver', label: '간수치', value: '정상', normal: '정상', status: 'good' },
]);
const recommendations = [
{
trigger: '혈압 높음',
insurance: '뇌혈관질환 진단비',
reason: '고혈압은 뇌졸중/뇌경색 위험 3배 증가',
priority: '긴급',
},
{
trigger: '콜레스테롤 높음',
insurance: '허혈성심장질환 특약',
reason: '심근경색 위험 2배 증가',
priority: '권장',
},
{
trigger: '대장 용종 발견',
insurance: '암보험 점검 필요',
reason: '대장암 예방 차원에서 진단비 확인',
priority: '긴급',
},
{
trigger: 'BMI 높음',
insurance: '당뇨 진단비 특약',
reason: '당뇨/대사증후군 발병 가능성',
priority: '권장',
},
];
return (
<ScreenContainer>
<Header title="건강검진 분석" />
<View style={{ padding: spacing.lg }}>
<Card>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Ionicons name="fitness" size={28} color={colors.danger} />
<Text style={{ ...typography.h3, marginLeft: 10 } as any}> </Text>
</View>
<Text style={{ ...typography.caption, color: colors.textSecondary, marginTop: 4 } as any}>
</Text>
<View style={{ marginTop: 16, gap: 10 }}>
{metrics.map((m) => (
<View key={m.key} style={styles.metricRow}>
<View style={{ flex: 1 }}>
<Text style={typography.bodyBold as any}>{m.label}</Text>
<Text style={{ ...typography.small, color: colors.textSecondary } as any}>: {m.normal}</Text>
</View>
<Text style={{ ...typography.title as any, color: statusColor(m.status), marginRight: 8 }}>
{m.value}
</Text>
<Badge
label={m.status === 'good' ? '정상' : m.status === 'warn' ? '주의' : '위험'}
tone={m.status === 'good' ? 'success' : m.status === 'warn' ? 'warning' : 'danger'}
/>
</View>
))}
</View>
</Card>
<Section title="🩺 검진 결과 기반 추천">
{recommendations.map((r, i) => (
<Card key={i} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 6 }}>
<Badge label={r.priority} tone={r.priority === '긴급' ? 'danger' : 'warning'} />
<Text style={{ ...typography.caption, marginLeft: 8, color: colors.textSecondary } as any}>
{r.trigger}
</Text>
</View>
<Text style={typography.h3 as any}>{r.insurance}</Text>
<Text style={{ ...typography.caption, color: colors.textSecondary, marginTop: 4 } as any}>
{r.reason}
</Text>
</Card>
))}
</Section>
<Section title="📝 검진 이력 관리">
<Card>
<Text style={typography.body as any}>
/PDF로 .
</Text>
<View style={{ height: 12 }} />
<Button title="검진 결과 업로드" variant="outline" />
</Card>
</Section>
</View>
</ScreenContainer>
);
}
function statusColor(s: 'good' | 'warn' | 'bad') {
return s === 'good' ? colors.success : s === 'warn' ? colors.warning : colors.danger;
}
const styles = StyleSheet.create({
metricRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
});
+130
View File
@@ -0,0 +1,130 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import Badge from '@/components/Badge';
import { useAppStore } from '@/store/useAppStore';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
export default function HiddenMoneyScreen() {
const hidden = useAppStore((s) => s.hiddenMoney);
const [state, setState] = useState<'idle' | 'loading' | 'result'>('idle');
const total = hidden.unclaimed + hidden.dormant;
const startSearch = () => {
setState('loading');
setTimeout(() => setState('result'), 1800);
};
return (
<ScreenContainer>
<Header title="숨은 보험금 조회" />
<View style={{ padding: spacing.lg }}>
<LinearGradient colors={colors.gradient.success} style={styles.hero}>
<Ionicons name="cash" size={44} color="#FFF" />
<Text style={styles.heroTitle}> , </Text>
<Text style={styles.heroSub}>
</Text>
</LinearGradient>
{state === 'idle' && (
<>
<Card style={{ marginTop: 16 }}>
<Text style={typography.bodyBold as any}>📋 </Text>
<View style={{ marginTop: 8, gap: 6 }}>
{['미청구 보험금 (만기/중도/사고)', '휴면 보험금 (3년 이상 미수령)', '실효 계약 환급금'].map((x) => (
<View key={x} style={{ flexDirection: 'row', alignItems: 'center' }}>
<Ionicons name="checkmark-circle" size={16} color={colors.success} />
<Text style={[typography.body as any, { marginLeft: 6 }]}>{x}</Text>
</View>
))}
</View>
</Card>
<View style={{ marginTop: 20 }}>
<Button title="숨은보험금 조회하기" size="lg" onPress={startSearch} />
</View>
<Text style={[styles.dim, { textAlign: 'center', marginTop: 8 }]}>
· ·
</Text>
</>
)}
{state === 'loading' && (
<Card style={{ marginTop: 16, alignItems: 'center', padding: 40 }}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={{ ...typography.bodyBold, marginTop: 16 } as any}> ...</Text>
<Text style={[styles.dim, { marginTop: 6 }]}> 3 </Text>
</Card>
)}
{state === 'result' && (
<>
<Card style={{ marginTop: 16 }}>
<Badge label="조회 완료" tone="success" />
<Text style={{ ...typography.h1 as any, color: colors.success, marginTop: 8 }}>
{total.toLocaleString()}
</Text>
<Text style={styles.dim}>
{hidden.unclaimed.toLocaleString()} + {hidden.dormant.toLocaleString()}
</Text>
<View style={{ height: 12 }} />
<Button title="지금 바로 청구하기" onPress={() => {}} />
</Card>
<Section title="발견된 보험금 내역">
{hidden.items.map((it, i) => (
<Card key={i} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={styles.iconBox}>
<Ionicons name="business" size={20} color={colors.primary} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={typography.bodyBold as any}>{it.insurer}</Text>
<Text style={styles.dim}>{it.type}</Text>
</View>
<Text style={[typography.title as any, { color: colors.success }]}>
{it.amount.toLocaleString()}
</Text>
</View>
</Card>
))}
</Section>
<Section title="📌 이런 것도 있어요">
<Card>
<Text style={typography.body as any}>
// {'\n'}
/ {'\n'}
10
</Text>
</Card>
</Section>
</>
)}
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
hero: { padding: 24, borderRadius: radius.xl, alignItems: 'center' },
heroTitle: { color: '#FFF', fontSize: 22, fontWeight: '800', marginTop: 12 },
heroSub: { color: 'rgba(255,255,255,0.9)', fontSize: 13, textAlign: 'center', marginTop: 6 },
dim: { ...typography.caption, color: colors.textSecondary, marginTop: 4 },
iconBox: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.primaryLight,
alignItems: 'center',
justifyContent: 'center',
},
});
+228
View File
@@ -0,0 +1,228 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import Card from '@/components/Card';
import Section from '@/components/Section';
import IconTile from '@/components/IconTile';
import ProgressBar from '@/components/ProgressBar';
import Badge from '@/components/Badge';
import { colors } from '@/theme/colors';
import { radius, shadow, spacing, typography } from '@/theme/typography';
import { useAppStore } from '@/store/useAppStore';
import type { RootStackParamList } from '@/navigation/RootNavigator';
type Nav = NativeStackNavigationProp<RootStackParamList>;
export default function HomeScreen() {
const nav = useNavigation<Nav>();
const profile = useAppStore((s) => s.profile);
const score = useAppStore((s) => s.score);
const hiddenMoney = useAppStore((s) => s.hiddenMoney);
const notifications = useAppStore((s) => s.notifications);
return (
<SafeAreaView style={styles.safe} edges={['top']}>
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 32 }}>
<View style={styles.topBar}>
<View>
<Text style={styles.brand}></Text>
<Text style={styles.brandSub}> , </Text>
</View>
<View style={styles.topIcons}>
<Ionicons
name="notifications-outline"
size={24}
color={colors.text}
onPress={() => nav.navigate('Notifications')}
/>
<View style={styles.dot} />
</View>
</View>
<LinearGradient
colors={colors.gradient.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.hero}
>
<Text style={styles.heroHello}>{profile.name}, 👋</Text>
<View style={styles.heroRow}>
<View style={{ flex: 1 }}>
<Text style={styles.heroLabel}> </Text>
<Text style={styles.heroScore}>
{score.total}
<Text style={styles.heroScoreUnit}> /100</Text>
</Text>
<View style={{ height: 10 }} />
<ProgressBar value={score.total} color="#FFF" track="rgba(255,255,255,0.3)" height={6} />
</View>
<View style={styles.heroBadge}>
<Ionicons name="shield-checkmark" size={36} color="#FFF" />
</View>
</View>
<View style={styles.heroActions}>
<TouchableOpacity style={styles.heroAction} activeOpacity={0.8} onPress={() => nav.navigate('Score')}>
<Text style={styles.heroActionText}> </Text>
<Ionicons name="chevron-forward" size={16} color="#FFF" />
</TouchableOpacity>
</View>
</LinearGradient>
<Section title="3초 진단" subtitle="질문 몇 개로 내 보험 상태 체크">
<Card onPress={() => nav.navigate('Diagnosis')} padding="lg">
<View style={styles.rowBetween}>
<View style={{ flex: 1 }}>
<Badge label="무료 · 3초 완료" tone="primary" />
<Text style={[typography.title as any, { marginTop: 10 }]}> </Text>
<Text style={styles.dim}>// </Text>
</View>
<LinearGradient colors={colors.gradient.primary} style={styles.diagIcon}>
<Ionicons name="flash" size={28} color="#FFF" />
</LinearGradient>
</View>
</Card>
</Section>
<Section title="주요 기능" subtitle="모든 보험 관리를 한 곳에서">
<Card padding="md">
<View style={styles.grid}>
<IconTile icon="stats-chart" label="점수" color="#8B5CF6" bg="#EDE9FE" onPress={() => nav.navigate('Score')} />
<IconTile icon="people" label="연령별 분석" color="#EC4899" bg="#FCE7F3" onPress={() => nav.navigate('Analysis')} />
<IconTile icon="cash" label="숨은보험금" color="#10B981" bg={colors.secondaryLight} badge="47만" onPress={() => nav.navigate('HiddenMoney')} />
<IconTile icon="search" label="질병코드" color="#F59E0B" bg={colors.accentLight} onPress={() => nav.navigate('DiseaseCode')} />
<IconTile icon="receipt" label="보험금 청구" color={colors.primary} bg={colors.primaryLight} onPress={() => nav.navigate('Claim')} />
<IconTile icon="fitness" label="건강검진 분석" color="#EF4444" bg={colors.dangerLight} onPress={() => nav.navigate('HealthCheck')} />
<IconTile icon="home" label="가족 보험" color="#6366F1" bg="#E0E7FF" onPress={() => nav.navigate('Family')} />
<IconTile icon="medkit" label="병원 체크" color="#14B8A6" bg="#CCFBF1" onPress={() => nav.navigate('HospitalChecklist')} />
<IconTile icon="sparkles" label="AI 판정" color="#8B5CF6" bg="#EDE9FE" badge="AI" onPress={() => nav.navigate('AIJudge')} />
<IconTile icon="trending-down" label="보험료 다이어트" color="#F97316" bg={colors.warningLight} onPress={() => nav.navigate('PremiumDiet')} />
<IconTile icon="shield-half" label="실손 세대" color="#0EA5E9" bg="#E0F2FE" onPress={() => nav.navigate('SilsonGen')} />
<IconTile icon="alarm" label="만기 알림" color="#DC2626" bg={colors.dangerLight} onPress={() => nav.navigate('Notifications')} />
</View>
</Card>
</Section>
<Section title="💰 숨은 보험금 알림">
<Card onPress={() => nav.navigate('HiddenMoney')}>
<View style={styles.rowBetween}>
<View style={{ flex: 1 }}>
<Badge label="못 받은 보험금 발견" tone="success" />
<Text style={[typography.h2 as any, { marginTop: 8, color: colors.success }]}>
{(hiddenMoney.unclaimed + hiddenMoney.dormant).toLocaleString()}
</Text>
<Text style={styles.dim}> {hiddenMoney.unclaimed.toLocaleString()} + {hiddenMoney.dormant.toLocaleString()}</Text>
</View>
<Ionicons name="chevron-forward" size={22} color={colors.textTertiary} />
</View>
</Card>
</Section>
<Section title="🔔 다가오는 알림">
{notifications.map((n) => (
<Card key={n.id} style={{ marginBottom: 10 }} onPress={() => nav.navigate('Notifications')}>
<View style={styles.rowBetween}>
<View style={{ flex: 1 }}>
<Badge
label={n.tone === 'danger' ? '긴급' : n.tone === 'warn' ? '알림' : '안내'}
tone={n.tone === 'danger' ? 'danger' : n.tone === 'warn' ? 'warning' : 'primary'}
/>
<Text style={[typography.bodyBold as any, { marginTop: 6 }]}>{n.title}</Text>
<Text style={styles.dim}>{n.body}</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.textTertiary} />
</View>
</Card>
))}
</Section>
<Section title="💬 빠른 상담">
<View style={{ flexDirection: 'row', gap: 10 }}>
<Card style={{ flex: 1 }} onPress={() => nav.navigate('Consult')}>
<Ionicons name="chatbubble-ellipses" size={28} color={colors.primary} />
<Text style={[typography.bodyBold as any, { marginTop: 10 }]}> </Text>
<Text style={styles.dim}> 9~18</Text>
</Card>
<Card style={{ flex: 1 }} onPress={() => nav.navigate('Consult')}>
<Ionicons name="call" size={28} color={colors.secondary} />
<Text style={[typography.bodyBold as any, { marginTop: 10 }]}> </Text>
<Text style={styles.dim}> </Text>
</Card>
</View>
</Section>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: colors.background },
topBar: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
},
brand: { fontSize: 22, fontWeight: '800', color: colors.primary },
brandSub: { ...typography.caption, color: colors.textSecondary },
topIcons: { flexDirection: 'row', alignItems: 'center' },
dot: {
position: 'absolute',
top: 0,
right: 0,
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: colors.danger,
},
hero: {
marginHorizontal: spacing.lg,
borderRadius: radius.xl,
padding: spacing.xl,
...shadow.md,
},
heroHello: { color: 'rgba(255,255,255,0.9)', fontSize: 14, fontWeight: '500' },
heroRow: { flexDirection: 'row', alignItems: 'center', marginTop: 12 },
heroLabel: { color: 'rgba(255,255,255,0.8)', fontSize: 13 },
heroScore: { color: '#FFF', fontSize: 42, fontWeight: '800', lineHeight: 50 },
heroScoreUnit: { color: 'rgba(255,255,255,0.7)', fontSize: 18, fontWeight: '600' },
heroBadge: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: 'rgba(255,255,255,0.2)',
alignItems: 'center',
justifyContent: 'center',
},
heroActions: {
marginTop: 16,
flexDirection: 'row',
justifyContent: 'flex-end',
},
heroAction: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.2)',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: radius.pill,
},
heroActionText: { color: '#FFF', fontWeight: '600', fontSize: 13, marginRight: 4 },
rowBetween: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
dim: { ...typography.caption, color: colors.textSecondary, marginTop: 4 },
diagIcon: {
width: 56,
height: 56,
borderRadius: 28,
alignItems: 'center',
justifyContent: 'center',
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
});
+150
View File
@@ -0,0 +1,150 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Share } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import Badge from '@/components/Badge';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
type CheckItem = {
id: string;
text: string;
detail?: string;
critical?: boolean;
};
const items: CheckItem[] = [
{ id: '1', text: '진단서 꼭 받으세요', detail: '청구 필수 서류 (질병분류코드 포함)', critical: true },
{ id: '2', text: '영수증 원본 보관', detail: '분실 시 재발급 수수료 발생', critical: true },
{ id: '3', text: '진료비 세부내역서 요청', detail: '비급여 항목 청구 시 필수' },
{ id: '4', text: 'MRI/CT는 의사 소견서 필요', detail: '실손 청구 시 의학적 필요성 증명' },
{ id: '5', text: '도수치료는 1회 3만원 이상만 청구 가능', detail: '3세대 이후 실손 기준' },
{ id: '6', text: '실손 청구 기한: 3년 이내', detail: '소멸시효 주의' },
{ id: '7', text: '수술받으면 수술명/코드 확인', detail: '수술비 특약 청구 시 코드 필수' },
{ id: '8', text: '입원일수 미리 체크', detail: '입원일당 특약 확인' },
];
const tipsByVisit = {
: ['진료비 영수증', '진단서 (1일 치료 1장 이상 필요 시)', '세부내역서'],
: ['입퇴원 확인서', '진단서', '진료비 세부내역서', '수술 받은 경우 수술확인서'],
: ['검사 결과지', '의사 소견서', '영수증 세부내역서'],
};
export default function HospitalChecklistScreen() {
const [checked, setChecked] = useState<Record<string, boolean>>({});
const [visitType, setVisitType] = useState<keyof typeof tipsByVisit>('외래');
const doneCount = Object.values(checked).filter(Boolean).length;
const share = async () => {
const text = items.map((it) => `${checked[it.id] ? '✅' : '⬜'} ${it.text}`).join('\n');
await Share.share({ message: `[병원 가기 전 체크리스트]\n\n${text}` });
};
return (
<ScreenContainer>
<Header title="병원 가기 전 체크리스트" />
<View style={{ padding: spacing.lg }}>
<Card>
<Text style={typography.bodyBold as any}>🏥 ?</Text>
<Text style={{ ...typography.caption, color: colors.textSecondary, marginTop: 4 } as any}>
</Text>
<View style={{ marginTop: 12, flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ ...typography.h2 as any, color: colors.primary }}>
{doneCount}/{items.length}
</Text>
<Text style={{ ...typography.body, marginLeft: 8, color: colors.textSecondary } as any}> </Text>
</View>
</Card>
<Section title="방문 유형 선택">
<View style={{ flexDirection: 'row', gap: 8 }}>
{(Object.keys(tipsByVisit) as Array<keyof typeof tipsByVisit>).map((t) => (
<TouchableOpacity
key={t}
style={[styles.typeBox, visitType === t && styles.typeBoxActive]}
onPress={() => setVisitType(t)}
>
<Text style={[styles.typeText, visitType === t && { color: '#FFF' }]}>{t}</Text>
</TouchableOpacity>
))}
</View>
<Card style={{ marginTop: 12 }}>
<Text style={typography.bodyBold as any}>📋 {visitType} </Text>
<View style={{ marginTop: 8 }}>
{tipsByVisit[visitType].map((t, i) => (
<Text key={i} style={{ ...typography.body, marginVertical: 2 } as any}>
{t}
</Text>
))}
</View>
</Card>
</Section>
<Section title="체크리스트">
{items.map((it) => (
<TouchableOpacity
key={it.id}
onPress={() => setChecked({ ...checked, [it.id]: !checked[it.id] })}
activeOpacity={0.8}
>
<Card style={{ marginBottom: 8 }}>
<View style={{ flexDirection: 'row' }}>
<View style={[styles.checkbox, checked[it.id] && styles.checkboxActive]}>
{checked[it.id] && <Ionicons name="checkmark" size={16} color="#FFF" />}
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text
style={[
typography.bodyBold as any,
checked[it.id] && { textDecorationLine: 'line-through', color: colors.textTertiary },
]}
>
{it.text}
</Text>
{it.critical && <Badge label="필수" tone="danger" style={{ marginLeft: 6 }} />}
</View>
{it.detail && <Text style={styles.detail}>{it.detail}</Text>}
</View>
</View>
</Card>
</TouchableOpacity>
))}
</Section>
<View style={{ paddingTop: 12 }}>
<Button title="체크리스트 공유하기" variant="outline" onPress={share} />
</View>
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
typeBox: {
flex: 1,
paddingVertical: 12,
borderRadius: radius.md,
backgroundColor: colors.surfaceAlt,
alignItems: 'center',
},
typeBoxActive: { backgroundColor: colors.primary },
typeText: { color: colors.text, fontWeight: '600' },
checkbox: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
borderColor: colors.border,
alignItems: 'center',
justifyContent: 'center',
},
checkboxActive: { backgroundColor: colors.primary, borderColor: colors.primary },
detail: { ...typography.caption, color: colors.textSecondary, marginTop: 2 },
});
+97
View File
@@ -0,0 +1,97 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Badge from '@/components/Badge';
import Button from '@/components/Button';
import { useAppStore } from '@/store/useAppStore';
import { colors } from '@/theme/colors';
import { spacing, typography } from '@/theme/typography';
import type { RootStackParamList } from '@/navigation/RootNavigator';
type Nav = NativeStackNavigationProp<RootStackParamList>;
export default function MyInsuranceScreen() {
const nav = useNavigation<Nav>();
const family = useAppStore((s) => s.family);
const profile = useAppStore((s) => s.profile);
const me = family.find((m) => m.relation === '본인');
const totalMonthly = me?.policies.reduce((a, p) => a + p.monthlyPremium, 0) ?? 0;
const totalCoverage = me?.policies.reduce((a, p) => a + p.coverage, 0) ?? 0;
return (
<ScreenContainer>
<Header title="내 보험" showBack={false} />
<Card style={{ margin: spacing.lg }}>
<Text style={styles.label}> </Text>
<Text style={styles.big}>{totalMonthly.toLocaleString()}</Text>
<View style={styles.divider} />
<View style={{ flexDirection: 'row' }}>
<View style={{ flex: 1 }}>
<Text style={styles.label}> </Text>
<Text style={styles.mid}>{(totalCoverage / 10000).toLocaleString()}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.label}> </Text>
<Text style={styles.mid}>{me?.policies.length ?? 0}</Text>
</View>
</View>
</Card>
<Section title="내 보험 목록">
{me?.policies.map((p) => (
<Card key={p.id} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={styles.icon}>
<Ionicons name="shield-checkmark" size={22} color={colors.primary} />
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
<Badge label={p.type} tone="primary" />
<Text style={styles.insurer}>{p.insurer}</Text>
</View>
<Text style={styles.name}>{p.name}</Text>
<Text style={styles.dim}>
{p.monthlyPremium.toLocaleString()} · {(p.coverage / 10000).toLocaleString()}
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.textTertiary} />
</View>
</Card>
))}
</Section>
<Section title="바로가기">
<View style={{ gap: 8 }}>
<Button title="내 보험 점수 상세보기" onPress={() => nav.navigate('Score')} />
<Button title="보험료 다이어트 진단" variant="outline" onPress={() => nav.navigate('PremiumDiet')} />
<Button title="실손 세대 판별" variant="outline" onPress={() => nav.navigate('SilsonGen')} />
<Button title="가족 보험 한눈에 보기" variant="outline" onPress={() => nav.navigate('Family')} />
</View>
</Section>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
label: { ...typography.caption, color: colors.textSecondary },
big: { fontSize: 32, fontWeight: '800', color: colors.text, marginTop: 4 },
mid: { fontSize: 18, fontWeight: '700', color: colors.text, marginTop: 4 },
divider: { height: 1, backgroundColor: colors.border, marginVertical: 16 },
icon: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.primaryLight,
alignItems: 'center',
justifyContent: 'center',
},
insurer: { ...typography.caption, color: colors.textSecondary },
name: { ...typography.bodyBold, color: colors.text, marginTop: 2 },
dim: { ...typography.caption, color: colors.textSecondary, marginTop: 2 },
});
+88
View File
@@ -0,0 +1,88 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Badge from '@/components/Badge';
import { useAppStore } from '@/store/useAppStore';
import { colors } from '@/theme/colors';
import { spacing, typography } from '@/theme/typography';
import type { RootStackParamList } from '@/navigation/RootNavigator';
type Nav = NativeStackNavigationProp<RootStackParamList>;
const menu = [
{ icon: 'person-circle', label: '개인정보 수정', route: undefined },
{ icon: 'shield-checkmark', label: '내 보험', route: 'MyInsurance' },
{ icon: 'people', label: '가족 보험', route: 'Family' },
{ icon: 'fitness', label: '건강검진 분석', route: 'HealthCheck' },
{ icon: 'notifications', label: '만기/갱신 알림 설정', route: 'Notifications' },
{ icon: 'lock-closed', label: '개인정보 처리방침', route: undefined },
{ icon: 'document-text', label: '이용약관', route: undefined },
{ icon: 'help-circle', label: '고객센터', route: 'Consult' },
] as const;
export default function MyPageScreen() {
const nav = useNavigation<Nav>();
const profile = useAppStore((s) => s.profile);
return (
<ScreenContainer>
<Header title="마이" showBack={false} />
<View style={{ padding: spacing.lg }}>
<Card>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={styles.avatar}>
<Ionicons name="person" size={32} color={colors.primary} />
</View>
<View style={{ flex: 1, marginLeft: 14 }}>
<Text style={typography.h3 as any}>{profile.name}</Text>
<Text style={styles.dim}>
{profile.age} · {profile.gender} · {profile.job}
</Text>
<View style={{ marginTop: 6 }}>
<Badge label={`보험점수 ${profile.score}`} tone="primary" />
</View>
</View>
</View>
</Card>
</View>
<Section title="설정">
{menu.map((m) => (
<Card
key={m.label}
style={{ marginBottom: 8 }}
onPress={() => (m.route ? nav.navigate(m.route as any) : null)}
>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Ionicons name={m.icon as any} size={22} color={colors.textSecondary} />
<Text style={[typography.body as any, { flex: 1, marginLeft: 12 }]}>{m.label}</Text>
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
</View>
</Card>
))}
</Section>
<View style={{ alignItems: 'center', marginTop: 24 }}>
<Text style={styles.dim}> v1.0.0</Text>
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
avatar: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: colors.primaryLight,
alignItems: 'center',
justifyContent: 'center',
},
dim: { ...typography.caption, color: colors.textSecondary, marginTop: 4 },
});
+154
View File
@@ -0,0 +1,154 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, Switch, TouchableOpacity } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import Badge from '@/components/Badge';
import { useAppStore } from '@/store/useAppStore';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
const upcomingItems = [
{ date: '11/15', title: '실손보험 갱신', detail: '보험료 7% 인상 예정 (12만원 → 12.8만원)', days: 22, severity: 'high' },
{ date: '11/28', title: '자녀 어린이보험 만기', detail: '연장 여부 결정 필요', days: 35, severity: 'mid' },
{ date: '12/03', title: '자동차보험 갱신', detail: '할인 조건 체크', days: 40, severity: 'low' },
{ date: '12/22', title: '종신보험 납입완료', detail: '20년 납입 완납 예정', days: 59, severity: 'low' },
];
export default function NotificationScreen() {
const [kakaoOn, setKakaoOn] = useState(true);
const [pushOn, setPushOn] = useState(true);
const [smsOn, setSmsOn] = useState(false);
const [timings, setTimings] = useState({ oneMonth: true, oneWeek: true, today: true, afterRenew: true });
const notifications = useAppStore((s) => s.notifications);
return (
<ScreenContainer>
<Header title="만기 · 갱신 알림" />
<View style={{ padding: spacing.lg }}>
<LinearGradient colors={['#DC2626', '#F97316']} style={styles.hero}>
<Ionicons name="alarm" size={40} color="#FFF" />
<Text style={styles.heroTitle}>🔔 </Text>
<Text style={styles.heroSub}> </Text>
</LinearGradient>
<Section title="📅 이번달 알림">
{upcomingItems.map((it, i) => (
<Card key={i} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={[styles.dateBox, { backgroundColor: severityColor(it.severity, 'bg') }]}>
<Text style={{ color: severityColor(it.severity, 'fg'), fontWeight: '800' }}>{it.date}</Text>
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Badge
label={`D-${it.days}`}
tone={it.severity === 'high' ? 'danger' : it.severity === 'mid' ? 'warning' : 'primary'}
/>
{it.severity === 'high' && <Text style={{ marginLeft: 6, fontSize: 16 }}>🚨</Text>}
</View>
<Text style={{ ...typography.bodyBold as any, marginTop: 4 }}>{it.title}</Text>
<Text style={{ ...typography.caption, color: colors.textSecondary, marginTop: 2 } as any}>
{it.detail}
</Text>
</View>
</View>
</Card>
))}
</Section>
<Section title="지난 알림">
{notifications.map((n) => (
<Card key={n.id} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ flex: 1 }}>
<Text style={typography.bodyBold as any}>{n.title}</Text>
<Text style={{ ...typography.caption, color: colors.textSecondary, marginTop: 2 } as any}>
{n.body}
</Text>
<Text style={{ ...typography.small, color: colors.textTertiary, marginTop: 4 } as any}>{n.date}</Text>
</View>
</View>
</Card>
))}
</Section>
<Section title="💬 알림톡 발송 시점">
<Card>
{[
{ key: 'oneMonth', label: '만기 1개월 전', icon: 'calendar' },
{ key: 'oneWeek', label: '만기 1주일 전', icon: 'calendar-outline' },
{ key: 'today', label: '만기 당일', icon: 'alert' },
{ key: 'afterRenew', label: '갱신 후 결과 안내', icon: 'checkmark-done' },
].map((s) => (
<View key={s.key} style={styles.switchRow}>
<Ionicons name={s.icon as any} size={20} color={colors.textSecondary} />
<Text style={{ ...typography.body as any, flex: 1, marginLeft: 10 }}>{s.label}</Text>
<Switch
value={(timings as any)[s.key]}
onValueChange={(v) => setTimings({ ...timings, [s.key]: v })}
trackColor={{ false: colors.border, true: colors.primary }}
thumbColor="#FFF"
/>
</View>
))}
</Card>
</Section>
<Section title="📱 알림 수신 방법">
<Card>
<View style={styles.switchRow}>
<Ionicons name="chatbubble" size={20} color="#F5B300" />
<Text style={{ ...typography.body as any, flex: 1, marginLeft: 10 }}> </Text>
<Switch value={kakaoOn} onValueChange={setKakaoOn} trackColor={{ false: colors.border, true: colors.primary }} thumbColor="#FFF" />
</View>
<View style={styles.switchRow}>
<Ionicons name="notifications" size={20} color={colors.primary} />
<Text style={{ ...typography.body as any, flex: 1, marginLeft: 10 }}> </Text>
<Switch value={pushOn} onValueChange={setPushOn} trackColor={{ false: colors.border, true: colors.primary }} thumbColor="#FFF" />
</View>
<View style={styles.switchRow}>
<Ionicons name="mail" size={20} color={colors.textSecondary} />
<Text style={{ ...typography.body as any, flex: 1, marginLeft: 10 }}>SMS </Text>
<Switch value={smsOn} onValueChange={setSmsOn} trackColor={{ false: colors.border, true: colors.primary }} thumbColor="#FFF" />
</View>
</Card>
</Section>
<View style={{ paddingTop: 16 }}>
<Button title="알림 설정 저장" size="lg" onPress={() => {}} />
</View>
</View>
</ScreenContainer>
);
}
function severityColor(s: string, kind: 'bg' | 'fg') {
if (s === 'high') return kind === 'bg' ? colors.dangerLight : colors.danger;
if (s === 'mid') return kind === 'bg' ? colors.warningLight : colors.warning;
return kind === 'bg' ? colors.primaryLight : colors.primary;
}
const styles = StyleSheet.create({
hero: { padding: 24, borderRadius: radius.xl, alignItems: 'center' },
heroTitle: { color: '#FFF', fontSize: 20, fontWeight: '800', marginTop: 10 },
heroSub: { color: 'rgba(255,255,255,0.9)', fontSize: 13, marginTop: 4 },
dateBox: {
width: 64,
height: 64,
borderRadius: radius.md,
alignItems: 'center',
justifyContent: 'center',
},
switchRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
});
+125
View File
@@ -0,0 +1,125 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import Badge from '@/components/Badge';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
const currentPremium = 472000;
const optimizedPremium = 318000;
const monthSaving = currentPremium - optimizedPremium;
const yearSaving = monthSaving * 12;
const duplications = [
{ title: '실손 + 단체실손 중복', severity: 'high', detail: '회사 단체실손 있음 → 개인 실손은 중복 보장 안됨', save: 32000 },
{ title: '암진단비 과다 (3건 가입)', severity: 'mid', detail: '진단비 9,000만원은 과다 보장. 5,000만원으로 조정 권장', save: 42000 },
{ title: '치아 + 실손 보장 중복', severity: 'low', detail: '스케일링/충치 치료 항목 중복', save: 15000 },
{ title: '저축성 보험 사업비 과다', severity: 'mid', detail: '10년 납입 기준 사업비 비율 13% → 7% 상품으로 변경 가능', save: 65000 },
];
export default function PremiumDietScreen() {
return (
<ScreenContainer>
<Header title="보험료 다이어트" />
<View style={{ padding: spacing.lg }}>
<LinearGradient colors={colors.gradient.warning} style={styles.hero}>
<Ionicons name="trending-down" size={40} color="#FFF" />
<Text style={styles.heroTitle}>💸 </Text>
<Text style={styles.heroSub}>· 15 </Text>
</LinearGradient>
<Card style={{ marginTop: 16 }}>
<Text style={typography.caption as any}> </Text>
<Text style={{ ...typography.h2 as any, color: colors.text, marginTop: 4 }}>
{currentPremium.toLocaleString()}
</Text>
<View style={styles.arrow}>
<Ionicons name="arrow-down" size={24} color={colors.success} />
</View>
<Text style={typography.caption as any}> </Text>
<Text style={{ ...typography.h2 as any, color: colors.success, marginTop: 4 }}>
{optimizedPremium.toLocaleString()}
</Text>
<View style={styles.resultBox}>
<View style={{ flex: 1 }}>
<Text style={styles.resultLabel}> </Text>
<Text style={styles.resultAmt}>{monthSaving.toLocaleString()}</Text>
</View>
<View style={styles.divider} />
<View style={{ flex: 1 }}>
<Text style={styles.resultLabel}> </Text>
<Text style={[styles.resultAmt, { color: colors.accent }]}>{yearSaving.toLocaleString()}</Text>
</View>
</View>
</Card>
<Section title="✂️ 발견된 중복·과다 보장">
{duplications.map((d, i) => (
<Card key={i} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ flex: 1 }}>
<Badge
label={d.severity === 'high' ? '즉시 조정' : d.severity === 'mid' ? '조정 권장' : '검토'}
tone={d.severity === 'high' ? 'danger' : d.severity === 'mid' ? 'warning' : 'primary'}
/>
<Text style={{ ...typography.bodyBold as any, marginTop: 6 }}>{d.title}</Text>
<Text style={{ ...typography.caption, color: colors.textSecondary, marginTop: 4 } as any}>
{d.detail}
</Text>
</View>
<View style={{ alignItems: 'flex-end', marginLeft: 10 }}>
<Text style={{ ...typography.small, color: colors.textSecondary } as any}> </Text>
<Text style={{ ...typography.title as any, color: colors.success }}>
-{d.save.toLocaleString()}
</Text>
</View>
</View>
</Card>
))}
</Section>
<Section title="💡 다이어트 팁">
<Card>
<Text style={typography.body as any}>
(1~3 ){'\n'}
1~2 {'\n'}
{'\n'}
65 +
</Text>
</Card>
</Section>
<View style={{ paddingTop: 16, gap: 8 }}>
<Button title="최적화 상담 받기" size="lg" onPress={() => {}} />
<Button title="리모델링 시뮬레이션" variant="outline" onPress={() => {}} />
</View>
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
hero: { padding: 24, borderRadius: radius.xl, alignItems: 'center' },
heroTitle: { color: '#FFF', fontSize: 22, fontWeight: '800', marginTop: 10 },
heroSub: { color: 'rgba(255,255,255,0.9)', fontSize: 13, marginTop: 6 },
arrow: { alignItems: 'center', marginVertical: 12 },
resultBox: {
flexDirection: 'row',
marginTop: 16,
padding: 14,
backgroundColor: colors.secondaryLight,
borderRadius: radius.md,
},
resultLabel: { ...typography.small, color: colors.textSecondary, fontWeight: '600' },
resultAmt: { fontSize: 18, fontWeight: '800', color: colors.success, marginTop: 2 },
divider: { width: 1, backgroundColor: colors.border },
});
+92
View File
@@ -0,0 +1,92 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import ScoreGauge from '@/components/ScoreGauge';
import ProgressBar from '@/components/ProgressBar';
import { useAppStore } from '@/store/useAppStore';
import { colors } from '@/theme/colors';
import { spacing, typography } from '@/theme/typography';
import type { RootStackParamList } from '@/navigation/RootNavigator';
type Nav = NativeStackNavigationProp<RootStackParamList>;
const statusMap = {
good: { icon: 'checkmark-circle', color: colors.success, label: '양호' },
warn: { icon: 'alert-circle', color: colors.warning, label: '부족' },
bad: { icon: 'close-circle', color: colors.danger, label: '취약' },
none: { icon: 'remove-circle', color: colors.textTertiary, label: '미가입' },
} as const;
export default function ScoreScreen() {
const nav = useNavigation<Nav>();
const score = useAppStore((s) => s.score);
return (
<ScreenContainer>
<Header title="내 보험 점수" />
<View style={{ padding: spacing.lg }}>
<Card padding="xl">
<View style={{ alignItems: 'center' }}>
<ScoreGauge value={score.total} size={200} />
</View>
<View style={{ marginTop: 16, padding: 14, backgroundColor: colors.primaryLight, borderRadius: 12 }}>
<Text style={{ color: colors.primaryDark, fontWeight: '700' }}>
💡 15% . 90 !
</Text>
</View>
</Card>
<Section title="항목별 점수">
{score.breakdown.map((b) => {
const s = statusMap[b.status];
return (
<Card key={b.label} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Ionicons name={s.icon as any} size={22} color={s.color} />
<Text style={[typography.bodyBold as any, { marginLeft: 8, flex: 1 }]}>{b.label}</Text>
<Text style={{ color: s.color, fontWeight: '700' }}>{s.label}</Text>
<Text style={{ ...typography.title as any, marginLeft: 10 }}>{b.value}</Text>
</View>
<View style={{ marginTop: 10 }}>
<ProgressBar value={b.value} color={s.color} />
</View>
</Card>
);
})}
</Section>
<Section title="⚡ 점수 올리는 법">
{[
{ title: '간병보험 가입', desc: '60대 이후 필수. +15점', route: 'Consult' },
{ title: '종신보험 보장 강화', desc: '자녀 자산 상속 대비. +10점', route: 'Consult' },
{ title: '치아보험 추가', desc: '중년기 치과 비용 대비. +5점', route: 'Consult' },
].map((a) => (
<Card key={a.title} style={{ marginBottom: 10 }} onPress={() => nav.navigate(a.route as any)}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ flex: 1 }}>
<Text style={typography.bodyBold as any}>{a.title}</Text>
<Text style={{ ...typography.caption, color: colors.textSecondary } as any}>{a.desc}</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.textTertiary} />
</View>
</Card>
))}
</Section>
<View style={{ paddingHorizontal: 0, paddingTop: 8, gap: 8 }}>
<Button title="맞춤 상담 받기" onPress={() => nav.navigate('Consult')} />
<Button title="보험료 다이어트 진단" variant="outline" onPress={() => nav.navigate('PremiumDiet')} />
</View>
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create({});
+191
View File
@@ -0,0 +1,191 @@
import React, { useState, useMemo } from 'react';
import { View, Text, StyleSheet, TextInput, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import ScreenContainer from '@/components/ScreenContainer';
import Header from '@/components/Header';
import Card from '@/components/Card';
import Section from '@/components/Section';
import Button from '@/components/Button';
import Badge from '@/components/Badge';
import { colors } from '@/theme/colors';
import { radius, spacing, typography } from '@/theme/typography';
import { silsonGenTable, identifyGenFromDate, SilsonGen } from '@/data/silsonGen';
export default function SilsonGenScreen() {
const [joinDate, setJoinDate] = useState('2015-06-10');
const detectedGen: SilsonGen | null = useMemo(() => {
try {
const d = new Date(joinDate);
if (Number.isNaN(d.getTime())) return null;
return identifyGenFromDate(joinDate);
} catch {
return null;
}
}, [joinDate]);
const info = detectedGen ? silsonGenTable.find((s) => s.generation === detectedGen)! : null;
return (
<ScreenContainer>
<Header title="실손 세대 판별" />
<View style={{ padding: spacing.lg }}>
<Card>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Ionicons name="shield-half" size={28} color="#0EA5E9" />
<Text style={{ ...typography.h3, marginLeft: 10 } as any}> ?</Text>
</View>
<Text style={{ ...typography.caption, color: colors.textSecondary, marginTop: 4 } as any}>
</Text>
<Text style={[styles.label, { marginTop: 16 }]}> </Text>
<TextInput
style={styles.input}
value={joinDate}
onChangeText={setJoinDate}
placeholder="YYYY-MM-DD"
placeholderTextColor={colors.textTertiary}
/>
<View style={{ flexDirection: 'row', gap: 6, marginTop: 8, flexWrap: 'wrap' }}>
{[
{ label: '2008년 이전', date: '2008-05-01' },
{ label: '2012년', date: '2012-06-15' },
{ label: '2018년', date: '2018-08-20' },
{ label: '2022년', date: '2022-03-10' },
{ label: '2025년 이후', date: '2025-06-01' },
].map((p) => (
<TouchableOpacity key={p.date} style={styles.pill} onPress={() => setJoinDate(p.date)}>
<Text style={styles.pillText}>{p.label}</Text>
</TouchableOpacity>
))}
</View>
</Card>
{info && (
<Card style={{ marginTop: 16 }}>
<Badge label="자동 판별 결과" tone="primary" />
<Text style={{ ...typography.h2 as any, marginTop: 10 }}>📌 {info.label}</Text>
<Text style={{ ...typography.caption, color: colors.textSecondary } as any}>{info.period}</Text>
<View style={styles.stats}>
<View style={styles.stat}>
<Text style={styles.statLabel}></Text>
<Text style={styles.statValue}>{info.selfPay}</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.stat}>
<Text style={styles.statLabel}> </Text>
<Text style={styles.statValue}>{info.renewCycle}</Text>
</View>
</View>
<View style={{ marginTop: 14 }}>
<Text style={styles.sub}> </Text>
{info.pros.map((p, i) => (
<Text key={i} style={{ ...typography.body, marginTop: 4, color: colors.text } as any}>
{p}
</Text>
))}
</View>
<View style={{ marginTop: 12 }}>
<Text style={styles.sub}> </Text>
{info.cons.map((p, i) => (
<Text key={i} style={{ ...typography.body, marginTop: 4, color: colors.text } as any}>
{p}
</Text>
))}
</View>
</Card>
)}
{info && info.generation !== 5 && (
<Section title="💡 5세대 전환 유불리 진단">
<Card>
<View style={{ flexDirection: 'row', gap: 12 }}>
<View style={{ flex: 1, padding: 14, backgroundColor: colors.primaryLight, borderRadius: radius.md }}>
<Text style={{ ...typography.small, color: colors.textSecondary } as any}> </Text>
<Text style={{ ...typography.h3 as any, color: colors.primary, marginTop: 4 }}>12</Text>
</View>
<View style={{ flex: 1, padding: 14, backgroundColor: colors.secondaryLight, borderRadius: radius.md }}>
<Text style={{ ...typography.small, color: colors.textSecondary } as any}>5 </Text>
<Text style={{ ...typography.h3 as any, color: colors.success, marginTop: 4 }}>4</Text>
</View>
</View>
<Text style={{ ...typography.bodyBold, marginTop: 12, color: colors.success } as any}>
8
</Text>
<Text style={{ ...typography.caption, color: colors.textSecondary, marginTop: 6 } as any}>
, / ,
</Text>
<View style={{ marginTop: 12 }}>
<Button title="전환 상담 받기" variant="outline" onPress={() => {}} />
</View>
</Card>
</Section>
)}
<Section title="📚 실손 세대 비교표">
{silsonGenTable.map((s) => (
<Card key={s.generation} style={{ marginBottom: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={styles.genBox}>
<Text style={styles.genText}>{s.generation}</Text>
</View>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={typography.bodyBold as any}>{s.label}</Text>
<Text style={{ ...typography.caption, color: colors.textSecondary } as any}>{s.period}</Text>
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={{ ...typography.small, color: colors.textSecondary } as any}></Text>
<Text style={{ ...typography.bodyBold as any }}>{s.selfPay}</Text>
</View>
</View>
</Card>
))}
</Section>
</View>
</ScreenContainer>
);
}
const styles = StyleSheet.create({
label: { ...typography.caption, color: colors.textSecondary, fontWeight: '600' },
input: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: radius.md,
padding: 14,
marginTop: 6,
fontSize: 16,
color: colors.text,
},
pill: {
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: radius.pill,
backgroundColor: colors.surfaceAlt,
borderWidth: 1,
borderColor: colors.border,
},
pillText: { ...typography.small, color: colors.text },
stats: {
flexDirection: 'row',
marginTop: 16,
padding: 14,
backgroundColor: colors.surfaceAlt,
borderRadius: radius.md,
},
stat: { flex: 1 },
statDivider: { width: 1, backgroundColor: colors.border, marginHorizontal: 12 },
statLabel: { ...typography.small, color: colors.textSecondary },
statValue: { ...typography.bodyBold, color: colors.text, marginTop: 4 },
sub: { ...typography.caption, color: colors.textSecondary, fontWeight: '700' },
genBox: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.primaryLight,
alignItems: 'center',
justifyContent: 'center',
},
genText: { color: colors.primary, fontWeight: '800', fontSize: 20 },
});
+172
View File
@@ -0,0 +1,172 @@
import { create } from 'zustand';
export type Policy = {
id: string;
name: string;
insurer: string;
type: '실손' | '암' | '종신' | '상해' | '어린이' | '간병' | '여성' | '치아' | '운전자' | '자동차';
monthlyPremium: number;
coverage: number;
joinDate: string;
maturityDate?: string;
isGroup?: boolean;
silsonGeneration?: 1 | 2 | 3 | 4 | 5;
renewalDate?: string;
};
export type FamilyMember = {
id: string;
relation: '본인' | '배우자' | '자녀' | '부모' | '형제';
name: string;
age: number;
gender: '남' | '여';
policies: Policy[];
};
export type Profile = {
name: string;
age: number;
gender: '남' | '여';
job: string;
monthlyPremium: number;
score: number;
};
type AppState = {
profile: Profile;
family: FamilyMember[];
score: { total: number; breakdown: Array<{ label: string; value: number; status: 'good' | 'warn' | 'bad' | 'none' }> };
hiddenMoney: { unclaimed: number; dormant: number; items: Array<{ insurer: string; type: string; amount: number }> };
claims: Array<{ id: string; title: string; status: '접수' | '심사' | '지급완료' | '서류보완'; date: string; amount?: number }>;
notifications: Array<{ id: string; title: string; body: string; date: string; tone: 'info' | 'warn' | 'danger' }>;
setProfile: (p: Partial<Profile>) => void;
};
export const useAppStore = create<AppState>((set) => ({
profile: {
name: '박철현',
age: 34,
gender: '남',
job: '사무직',
monthlyPremium: 472000,
score: 82,
},
family: [
{
id: 'me',
relation: '본인',
name: '박철현',
age: 34,
gender: '남',
policies: [
{
id: 'p1',
name: '4세대 실손의료비',
insurer: '삼성생명',
type: '실손',
monthlyPremium: 32000,
coverage: 50000000,
joinDate: '2021-07-01',
renewalDate: '2026-07-01',
silsonGeneration: 4,
},
{
id: 'p2',
name: '종합암보험',
insurer: '현대해상',
type: '암',
monthlyPremium: 58000,
coverage: 50000000,
joinDate: '2019-03-15',
},
],
},
{
id: 'sp',
relation: '배우자',
name: '김지영',
age: 32,
gender: '여',
policies: [
{
id: 'p3',
name: '4세대 실손의료비',
insurer: 'KB손해',
type: '실손',
monthlyPremium: 28000,
coverage: 50000000,
joinDate: '2022-01-10',
silsonGeneration: 4,
},
],
},
{
id: 'ch',
relation: '자녀',
name: '박서연',
age: 6,
gender: '여',
policies: [
{
id: 'p4',
name: '어린이보험',
insurer: '메리츠',
type: '어린이',
monthlyPremium: 45000,
coverage: 30000000,
joinDate: '2020-05-20',
},
],
},
{
id: 'pa',
relation: '부모',
name: '박영수',
age: 68,
gender: '남',
policies: [
{
id: 'p5',
name: '1세대 실손의료비',
insurer: '동부화재',
type: '실손',
monthlyPremium: 95000,
coverage: 30000000,
joinDate: '2008-06-12',
silsonGeneration: 1,
},
],
},
],
score: {
total: 82,
breakdown: [
{ label: '실손보험', value: 95, status: 'good' },
{ label: '암보험', value: 90, status: 'good' },
{ label: '종신보험', value: 60, status: 'warn' },
{ label: '간병보험', value: 0, status: 'none' },
{ label: '상해보험', value: 80, status: 'good' },
{ label: '치아보험', value: 40, status: 'bad' },
],
},
hiddenMoney: {
unclaimed: 470000,
dormant: 120000,
items: [
{ insurer: '삼성생명', type: '만기환급금', amount: 320000 },
{ insurer: '교보생명', type: '중도보험금', amount: 150000 },
{ insurer: '한화손해', type: '휴면보험금', amount: 120000 },
],
},
claims: [
{ id: 'c1', title: '감기 통원 진료비', status: '지급완료', date: '2026-03-11', amount: 38000 },
{ id: 'c2', title: '발목 염좌 정형외과', status: '심사', date: '2026-04-18', amount: 120000 },
{ id: 'c3', title: '건강검진 위내시경', status: '서류보완', date: '2026-04-20' },
],
notifications: [
{ id: 'n1', title: '실손보험 갱신 예정', body: '11/15 갱신 - 7% 인상 예정 (12만원 → 12.8만원)', date: '2026-04-25', tone: 'warn' },
{ id: 'n2', title: '자녀 어린이보험 만기', body: '11/28 만기 - 연장 여부 결정 필요', date: '2026-04-28', tone: 'info' },
{ id: 'n3', title: '숨은보험금 47만원 발견', body: '미청구 보험금 조회 결과 확인', date: '2026-04-15', tone: 'danger' },
],
setProfile: (p) => set((s) => ({ profile: { ...s.profile, ...p } })),
}));
+38
View File
@@ -0,0 +1,38 @@
export const colors = {
primary: '#3B82F6',
primaryDark: '#2563EB',
primaryLight: '#DBEAFE',
secondary: '#10B981',
secondaryLight: '#D1FAE5',
accent: '#F59E0B',
accentLight: '#FEF3C7',
danger: '#EF4444',
dangerLight: '#FEE2E2',
warning: '#F97316',
warningLight: '#FFEDD5',
text: '#111827',
textSecondary: '#6B7280',
textTertiary: '#9CA3AF',
background: '#F9FAFB',
surface: '#FFFFFF',
surfaceAlt: '#F3F4F6',
border: '#E5E7EB',
borderDark: '#D1D5DB',
success: '#10B981',
info: '#3B82F6',
kakao: '#FEE500',
kakaoText: '#191600',
gradient: {
primary: ['#3B82F6', '#2563EB'] as [string, string],
success: ['#10B981', '#059669'] as [string, string],
warning: ['#F59E0B', '#D97706'] as [string, string],
danger: ['#EF4444', '#DC2626'] as [string, string],
purple: ['#8B5CF6', '#7C3AED'] as [string, string],
pink: ['#EC4899', '#DB2777'] as [string, string],
},
};
+55
View File
@@ -0,0 +1,55 @@
import { TextStyle } from 'react-native';
export const typography: Record<string, TextStyle> = {
h1: { fontSize: 28, fontWeight: '800', lineHeight: 36 },
h2: { fontSize: 22, fontWeight: '700', lineHeight: 30 },
h3: { fontSize: 18, fontWeight: '700', lineHeight: 26 },
title: { fontSize: 16, fontWeight: '700', lineHeight: 24 },
body: { fontSize: 15, fontWeight: '400', lineHeight: 22 },
bodyBold: { fontSize: 15, fontWeight: '600', lineHeight: 22 },
caption: { fontSize: 13, fontWeight: '400', lineHeight: 18 },
small: { fontSize: 12, fontWeight: '400', lineHeight: 16 },
button: { fontSize: 16, fontWeight: '700', lineHeight: 24 },
};
export const spacing = {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 20,
xxl: 24,
xxxl: 32,
};
export const radius = {
sm: 6,
md: 10,
lg: 14,
xl: 18,
pill: 999,
};
export const shadow = {
sm: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
md: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 6,
elevation: 3,
},
lg: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.12,
shadowRadius: 12,
elevation: 6,
},
};
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["**/*.ts", "**/*.tsx"]
}