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:
+16
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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` 실행 시 모든 화면을 브라우저에서 볼 수 있습니다.
|
||||
단, 카메라(보험금 청구)/푸시알림 같은 네이티브 기능은 실기기/에뮬레이터에서 확인해야 합니다.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
],
|
||||
};
|
||||
};
|
||||
Generated
+14704
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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' },
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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' },
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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' },
|
||||
});
|
||||
@@ -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' },
|
||||
});
|
||||
@@ -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' },
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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' },
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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' },
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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({});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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 } })),
|
||||
}));
|
||||
@@ -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],
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
Reference in New Issue
Block a user