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