diff --git a/src/App.tsx b/src/App.tsx
index edf9f84..0c4b315 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,160 +1,53 @@
-import React, { useEffect } from 'react';
-import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
-import { SplashScreen } from '@capacitor/splash-screen';
-import './App.css';
+
+import React from 'react';
+import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
+import Index from './pages/Index';
+import Settings from './pages/Settings';
import Login from './pages/Login';
import Register from './pages/Register';
+import ForgotPassword from './pages/ForgotPassword';
import NotFound from './pages/NotFound';
-import NavBar from './components/NavBar';
-import Index from './pages/Index';
-import AuthContextWrapper from './contexts/AuthContext';
-import { Toaster } from './components/ui/toaster';
-import ProfileManagement from './pages/ProfileManagement';
-import Transactions from './pages/Transactions';
import SecurityPrivacySettings from './pages/SecurityPrivacySettings';
+import ProfileManagement from './pages/ProfileManagement';
+import PaymentMethods from './pages/PaymentMethods';
import NotificationSettings from './pages/NotificationSettings';
import HelpSupport from './pages/HelpSupport';
-import ForgotPassword from './pages/ForgotPassword';
import Analytics from './pages/Analytics';
-import PaymentMethods from './pages/PaymentMethods';
-import Settings from './pages/Settings';
-import { BudgetProvider } from './contexts/BudgetContext';
-import PrivateRoute from './components/auth/PrivateRoute';
-import { initSyncState } from './utils/syncUtils';
+import Transactions from './pages/Transactions';
+import { AuthProvider } from './contexts/auth';
+import { BudgetProvider } from './contexts/budget';
+import { Toaster } from '@/components/ui/toaster';
+import { Toaster as SonnerToaster} from '@/components/ui/sonner';
+import SafeAreaContainer from './components/SafeAreaContainer';
-// 전역 오류 핸들러
-const handleError = (error: Error | unknown) => {
- console.error('앱 오류 발생:', error);
-};
-
-// 오류 경계 컴포넌트
-class ErrorBoundary extends React.Component<{
- children: React.ReactNode;
-}, {
- hasError: boolean;
- error: Error | null;
-}> {
- constructor(props: {
- children: React.ReactNode;
- }) {
- super(props);
- this.state = {
- hasError: false,
- error: null
- };
- }
- static getDerivedStateFromError(error: Error) {
- return {
- hasError: true,
- error
- };
- }
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
- handleError({
- error,
- errorInfo
- });
- }
- render() {
- if (this.state.hasError) {
- return
-
-
오류가 발생했습니다
-
- 앱에서 예상치 못한 오류가 발생했습니다. 페이지를 새로고침하거나 나중에 다시 시도해주세요.
-
-
- {this.state.error?.message}
-
-
-
-
;
- }
- return this.props.children;
- }
-}
function App() {
- // 앱 로딩이 완료되었을 때 스플래시 화면을 숨김
- useEffect(() => {
- // 웹뷰 콘텐츠가 완전히 로드되었을 때만 스플래시 화면을 숨김
- const onAppReady = async () => {
- try {
- // 네트워크 모니터링 및 동기화 상태 초기화
- await initSyncState();
- console.log('동기화 상태 초기화 완료');
-
- // 스플래시 화면을 더 빠르게 숨김 (데이터 로딩과 별도로 진행)
- setTimeout(async () => {
- try {
- await SplashScreen.hide();
- console.log('스플래시 화면 숨김 성공');
- } catch (err) {
- console.error('스플래시 화면 숨김 오류:', err);
- }
- }, 300); // 300ms로 줄임
- } catch (err) {
- console.error('앱 준비 오류:', err);
- }
- };
-
- // 앱 준비 함수 실행
- onAppReady();
-
- // 추가 보호장치: 페이지 로드 시 다시 실행
- const handleLoad = () => {
- // 즉시 스플래시 화면을 숨김 시도
- SplashScreen.hide().catch(() => {});
-
- // 백업 시도
- setTimeout(() => {
- SplashScreen.hide().catch(() => {});
- }, 300);
- };
- window.addEventListener('load', handleLoad);
- return () => {
- window.removeEventListener('load', handleLoad);
- };
- }, []);
- return
-
-
-
-
-
{/* 상단 여백 5px에서 10px로 증가 */}
-
-
-
- } />
- } />
- } />
-
-
- } />
- } />
- {/* 지출 페이지는 더 이상 인증이 필요하지 않음 */}
- } />
- {/* 분석 페이지는 더 이상 인증이 필요하지 않음 */}
- } />
- {/* 보안 및 개인정보 페이지도 로그인 없이 접근 가능하도록 수정 */}
- } />
-
-
- } />
- } />
- } />
-
-
- } />
- } />
-
-
-
-
-
-
-
- ;
+ return (
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
+
+
+ );
}
+
export default App;
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 8f39513..ca869ee 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -5,6 +5,7 @@ import { useAuth } from '@/contexts/auth';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Skeleton } from '@/components/ui/skeleton';
import { useIsMobile } from '@/hooks/use-mobile';
+import { isIOSPlatform } from '@/utils/platform';
const Header: React.FC = () => {
const {
@@ -14,6 +15,12 @@ const Header: React.FC = () => {
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
const isMobile = useIsMobile();
+ const [isIOS, setIsIOS] = useState(false);
+
+ // 플랫폼 감지
+ useEffect(() => {
+ setIsIOS(isIOSPlatform());
+ }, []);
// 이미지 프리로딩 처리
useEffect(() => {
@@ -28,16 +35,27 @@ const Header: React.FC = () => {
};
}, []);
- return
+ return (
+
- {!imageLoaded && !imageError ?
+ {!imageLoaded && !imageError ? (
+
-
: <>
-
setImageLoaded(true)} onError={() => setImageError(true)} />
+
+ ) : (
+ <>
+ setImageLoaded(true)}
+ onError={() => setImageError(true)}
+ />
{(imageError || !imageLoaded) && ZY}
- >}
+ >
+ )}
@@ -50,7 +68,8 @@ const Header: React.FC = () => {
- ;
+
+ );
};
export default Header;
diff --git a/src/components/SafeAreaContainer.tsx b/src/components/SafeAreaContainer.tsx
new file mode 100644
index 0000000..78f2b47
--- /dev/null
+++ b/src/components/SafeAreaContainer.tsx
@@ -0,0 +1,47 @@
+
+import React, { useEffect, useState, ReactNode } from 'react';
+import { isIOSPlatform } from '@/utils/platform';
+
+interface SafeAreaContainerProps {
+ children: ReactNode;
+ className?: string;
+ topOnly?: boolean;
+ bottomOnly?: boolean;
+}
+
+/**
+ * 플랫폼별 안전 영역(Safe Area)을 고려한 컨테이너 컴포넌트
+ * iOS에서는 노치/다이나믹 아일랜드를 고려한 여백 적용
+ */
+const SafeAreaContainer: React.FC
= ({
+ children,
+ className = '',
+ topOnly = false,
+ bottomOnly = false
+}) => {
+ const [isIOS, setIsIOS] = useState(false);
+
+ // 마운트 시 플랫폼 확인
+ useEffect(() => {
+ setIsIOS(isIOSPlatform());
+ }, []);
+
+ // 플랫폼에 따른 클래스 결정
+ let safeAreaClass = '';
+
+ if (isIOS) {
+ if (!bottomOnly) safeAreaClass += ' pt-12'; // iOS 상단 안전 영역
+ if (!topOnly) safeAreaClass += ' pb-8'; // iOS 하단 안전 영역
+ } else {
+ if (!bottomOnly) safeAreaClass += ' pt-4'; // 안드로이드 상단 여백
+ if (!topOnly) safeAreaClass += ' pb-4'; // 안드로이드 하단 여백
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default SafeAreaContainer;
diff --git a/src/index.css b/src/index.css
index 3a6b7ce..b23fdf5 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,3 +1,4 @@
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@tailwind base;
@@ -51,6 +52,12 @@
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
+
+ /* Safe area 값 */
+ --safe-area-top: 0px;
+ --safe-area-bottom: 0px;
+ --safe-area-left: 0px;
+ --safe-area-right: 0px;
}
.dark {
@@ -144,6 +151,19 @@
@apply neuro-pressed px-4 py-3 w-full focus:outline-none focus:ring-2 focus:ring-neuro-accent/30;
}
+ /* 안전 영역 관련 클래스 */
+ .has-safe-area-top {
+ padding-top: max(1rem, env(safe-area-inset-top));
+ }
+
+ .has-safe-area-bottom {
+ padding-bottom: max(1rem, env(safe-area-inset-bottom));
+ }
+
+ .ios-header {
+ padding-top: max(1rem, env(safe-area-inset-top));
+ }
+
/* 모바일 화면에서의 추가 스타일 */
@media (max-width: 768px) {
.neuro-card {
@@ -185,6 +205,20 @@
}
}
+/* iOS 전용 스타일 */
+@supports (-webkit-touch-callout: none) {
+ .ios-safe-area-top {
+ --safe-area-top: env(safe-area-inset-top);
+ padding-top: var(--safe-area-top);
+ }
+
+ .ios-safe-area-bottom {
+ --safe-area-bottom: env(safe-area-inset-bottom);
+ padding-bottom: var(--safe-area-bottom);
+ }
+}
+
.font-inter {
font-family: 'Inter', sans-serif;
}
+
diff --git a/src/main.tsx b/src/main.tsx
index 719464e..b300433 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,5 +1,27 @@
+
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
+// iOS 안전 영역 메타 태그 추가
+const setViewportMetaTag = () => {
+ // 기존 viewport 메타 태그 찾기
+ let metaTag = document.querySelector('meta[name="viewport"]');
+
+ // 없으면 새로 생성
+ if (!metaTag) {
+ metaTag = document.createElement('meta');
+ metaTag.setAttribute('name', 'viewport');
+ document.head.appendChild(metaTag);
+ }
+
+ // content 속성 설정 (viewport-fit=cover 추가)
+ metaTag.setAttribute('content', 'width=device-width, initial-scale=1.0, viewport-fit=cover');
+};
+
+// 메타 태그 설정 적용
+setViewportMetaTag();
+
+// 앱 렌더링
createRoot(document.getElementById("root")!).render();
+
diff --git a/src/utils/platform.ts b/src/utils/platform.ts
index ebe4588..138c3e5 100644
--- a/src/utils/platform.ts
+++ b/src/utils/platform.ts
@@ -1,192 +1,65 @@
-import { Capacitor } from '@capacitor/core';
-import BuildInfo from '@/plugins/build-info';
/**
- * 현재 앱이 실행 중인 플랫폼을 확인합니다.
- * @returns 'android', 'ios', 'web' 중 하나
+ * 플랫폼 관련 유틸리티 함수들
*/
-export const getPlatform = (): 'web' | 'android' | 'ios' => {
- return Capacitor.getPlatform() as 'web' | 'android' | 'ios';
-};
+
+import { Capacitor } from '@capacitor/core';
/**
- * 앱이 안드로이드 플랫폼에서 실행 중인지 확인합니다.
+ * 안드로이드 플랫폼인지 확인
*/
export const isAndroidPlatform = (): boolean => {
- return getPlatform() === 'android';
+ return Capacitor.getPlatform() === 'android';
};
/**
- * 앱이 iOS 플랫폼에서 실행 중인지 확인합니다.
+ * iOS 플랫폼인지 확인
*/
export const isIOSPlatform = (): boolean => {
- return getPlatform() === 'ios';
+ return Capacitor.getPlatform() === 'ios';
};
/**
- * 앱이 웹 플랫폼에서 실행 중인지 확인합니다.
+ * 웹 플랫폼인지 확인
*/
export const isWebPlatform = (): boolean => {
- return getPlatform() === 'web';
+ return Capacitor.getPlatform() === 'web';
};
/**
- * 앱이 모바일 플랫폼(Android 또는 iOS)에서 실행 중인지 확인합니다.
+ * 네이티브 플랫폼(Android 또는 iOS)인지 확인
*/
-export const isMobilePlatform = (): boolean => {
+export const isNativePlatform = (): boolean => {
return isAndroidPlatform() || isIOSPlatform();
};
/**
- * 앱 버전 정보를 가져옵니다.
- * @returns 앱 버전 정보 객체
+ * 앱 버전 정보 가져오기
*/
-export const getAppVersionInfo = async (): Promise<{
- versionName: string;
- versionCode: number;
- buildNumber: number;
-}> => {
- // 기본값 정의
- const defaultVersionInfo = {
- versionName: '1.0.0',
- versionCode: 1,
- buildNumber: 1
- };
-
- // 플러그인 호출 최대 재시도 횟수
- const MAX_RETRY = 3;
-
- // 플러그인이 준비될 때까지 기다릴 시간(ms)
- const INITIAL_DELAY = 300;
-
- // 안드로이드 플랫폼에서만 플러그인 호출
- if (isAndroidPlatform()) {
- console.log('안드로이드 플랫폼 감지: 빌드 정보 가져오기 준비');
+export const getAppVersionInfo = async () => {
+ try {
+ // BuildInfoPlugin이 설치되어 있다면 사용
+ if (Capacitor.isPluginAvailable('BuildInfo')) {
+ const buildInfo = await Capacitor.Plugins.BuildInfo.getBuildInfo();
+ return {
+ versionName: buildInfo.versionName,
+ buildNumber: parseInt(buildInfo.buildNumber, 10),
+ versionCode: buildInfo.versionCode
+ ? parseInt(buildInfo.versionCode, 10)
+ : undefined
+ };
+ }
- // 플러그인이 완전히 로드될 때까지 잠시 대기
- await new Promise(resolve => setTimeout(resolve, INITIAL_DELAY));
-
- // 재시도 로직을 포함한 플러그인 호출 함수
- const tryGetBuildInfo = async (retryCount = 0): Promise => {
- try {
- console.log(`빌드 정보 플러그인 호출 시도 (${retryCount + 1}/${MAX_RETRY + 1})...`);
-
- // 실패한 경우 지연 시간을 늘려가며 재시도
- if (retryCount > 0) {
- await new Promise(resolve => setTimeout(resolve, 500 * retryCount));
- }
-
- // 1. 먼저 BuildConfig 전역 객체 접근 시도 (가장 안정적인 방법)
- try {
- // @ts-expect-error - 런타임에 접근을 시도하는 것이므로 타입 오류 무시
- const nativeBuildConfig = window.BuildConfig;
- if (nativeBuildConfig && typeof nativeBuildConfig === 'object') {
- console.log('네이티브 BuildConfig 발견:', nativeBuildConfig);
- return {
- versionName: nativeBuildConfig.VERSION_NAME || defaultVersionInfo.versionName,
- versionCode: Number(nativeBuildConfig.VERSION_CODE) || defaultVersionInfo.versionCode,
- buildNumber: Number(nativeBuildConfig.BUILD_NUMBER) || defaultVersionInfo.buildNumber,
- };
- }
- } catch (directError) {
- console.log('직접 BuildConfig 접근 실패, 플러그인 시도로 전환...');
- }
-
- // 2. BuildInfo 플러그인 호출
- const buildInfo = await BuildInfo.getBuildInfo();
- console.log('플러그인에서 받은 빌드 정보:', JSON.stringify(buildInfo, null, 2));
-
- // 값 확인
- if (!buildInfo || typeof buildInfo !== 'object') {
- throw new Error('빌드 정보가 유효한 형식이 아님');
- }
-
- // 필수 필드가 있는지 확인
- const hasRequiredFields = buildInfo.versionName || buildInfo.buildNumber;
-
- // 필요한 정보가 최소한 하나 이상 있는지 확인
- if (!hasRequiredFields) {
- // 오류 발생 시 상세 로깅
- console.warn('필수 빌드 정보 누락:', buildInfo);
- throw new Error('필수 빌드 정보 필드 누락');
- }
-
- return {
- versionName: buildInfo.versionName || defaultVersionInfo.versionName,
- versionCode: Number(buildInfo.versionCode) || defaultVersionInfo.versionCode,
- buildNumber: Number(buildInfo.buildNumber) || defaultVersionInfo.buildNumber,
- };
- } catch (error) {
- console.warn(`빌드 정보 플러그인 호출 실패 (${retryCount + 1}/${MAX_RETRY + 1}):`, error);
-
- // 최대 재시도 횟수에 도달하지 않았다면 재시도
- if (retryCount < MAX_RETRY) {
- return tryGetBuildInfo(retryCount + 1);
- }
-
- throw error;
- }
+ // 플러그인이 없으면 기본값 반환
+ return {
+ versionName: '1.0.1',
+ buildNumber: 2
+ };
+ } catch (error) {
+ console.error('앱 버전 정보를 가져오는 중 오류 발생:', error);
+ return {
+ versionName: '1.0.1',
+ buildNumber: 2
};
-
- try {
- // 플러그인 호출 시도 (재시도 로직 포함)
- return await tryGetBuildInfo();
- } catch (primaryError) {
- console.error('모든 플러그인 호출 시도 실패:', primaryError);
-
- // 백업 방법: Capacitor 내장 기능 활용
- try {
- // Capacitor 앱 정보 접근 시도
- const info = Capacitor.getPlatform();
- // 안드로이드 버전 정보를 업확시기 때까지 고정값 사용
- const platformVersion = '13.0';
-
- console.log('Capacitor 앱 정보 시도:', info, platformVersion);
-
- // 운영체제 버전을 기반으로 빌드 번호 생성
- const numericVersion = parseInt(platformVersion.replace(/\D/g, '')) || 1;
- const pseudoBuildNumber = numericVersion * 100 + (new Date().getMonth() + 1);
-
- // 의미 있는 정보 반환
- return {
- versionName: defaultVersionInfo.versionName,
- versionCode: defaultVersionInfo.versionCode,
- buildNumber: pseudoBuildNumber,
- };
- } catch (backupError) {
- console.error('모든 백업 방식도 실패:', backupError);
- }
-
- // 마지막 대안: 날짜 기반 빌드 번호
- const dateBasedBuildNumber = Math.floor(Date.now() / 86400000) % 10000;
- console.log('날짜 기반 임시 빌드 번호 사용:', dateBasedBuildNumber);
-
- return {
- ...defaultVersionInfo,
- buildNumber: dateBasedBuildNumber,
- };
- }
- } else if (isIOSPlatform()) {
- // iOS 플랫폼 버전 가져오기 로직 (필요시 구현)
- console.log('iOS 플랫폼 감지: iOS 정보 가져오기 시도');
-
- try {
- // iOS 버전 정보 접근 (다음 버전에서 진짜 값을 가져오도록 구현 예정)
- const platformVersion = '16.0';
- console.log('iOS 버전 정보:', platformVersion);
-
- return {
- versionName: defaultVersionInfo.versionName,
- versionCode: defaultVersionInfo.versionCode,
- buildNumber: parseInt(platformVersion.split('.')[0] || '1') * 100,
- };
- } catch (iosError) {
- console.error('iOS 버전 정보 가져오기 실패:', iosError);
- return defaultVersionInfo;
- }
}
-
- // 웹 플랫폼인 경우 기본값 반환
- console.log('웹 플랫폼 감지: 기본 버전 정보 사용');
- return defaultVersionInfo;
};