Refactor: Adapt to design changes
The design has been significantly updated, requiring code adjustments.
This commit is contained in:
167
src/App.tsx
167
src/App.tsx
@@ -1,160 +1,53 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
import React from 'react';
|
||||||
import { SplashScreen } from '@capacitor/splash-screen';
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
import './App.css';
|
import Index from './pages/Index';
|
||||||
|
import Settings from './pages/Settings';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import Register from './pages/Register';
|
import Register from './pages/Register';
|
||||||
|
import ForgotPassword from './pages/ForgotPassword';
|
||||||
import NotFound from './pages/NotFound';
|
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 SecurityPrivacySettings from './pages/SecurityPrivacySettings';
|
||||||
|
import ProfileManagement from './pages/ProfileManagement';
|
||||||
|
import PaymentMethods from './pages/PaymentMethods';
|
||||||
import NotificationSettings from './pages/NotificationSettings';
|
import NotificationSettings from './pages/NotificationSettings';
|
||||||
import HelpSupport from './pages/HelpSupport';
|
import HelpSupport from './pages/HelpSupport';
|
||||||
import ForgotPassword from './pages/ForgotPassword';
|
|
||||||
import Analytics from './pages/Analytics';
|
import Analytics from './pages/Analytics';
|
||||||
import PaymentMethods from './pages/PaymentMethods';
|
import Transactions from './pages/Transactions';
|
||||||
import Settings from './pages/Settings';
|
import { AuthProvider } from './contexts/auth';
|
||||||
import { BudgetProvider } from './contexts/BudgetContext';
|
import { BudgetProvider } from './contexts/budget';
|
||||||
import PrivateRoute from './components/auth/PrivateRoute';
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
import { initSyncState } from './utils/syncUtils';
|
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 <div className="min-h-screen flex items-center justify-center p-4 bg-gray-50">
|
|
||||||
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
|
|
||||||
<h1 className="text-2xl font-bold text-red-600 mb-4">오류가 발생했습니다</h1>
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
앱에서 예상치 못한 오류가 발생했습니다. 페이지를 새로고침하거나 나중에 다시 시도해주세요.
|
|
||||||
</p>
|
|
||||||
<pre className="bg-gray-100 p-4 rounded text-xs overflow-auto max-h-40 mb-4">
|
|
||||||
{this.state.error?.message}
|
|
||||||
</pre>
|
|
||||||
<button onClick={() => window.location.reload()} className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 w-full">
|
|
||||||
페이지 새로고침
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function App() {
|
function App() {
|
||||||
// 앱 로딩이 완료되었을 때 스플래시 화면을 숨김
|
return (
|
||||||
useEffect(() => {
|
<AuthProvider>
|
||||||
// 웹뷰 콘텐츠가 완전히 로드되었을 때만 스플래시 화면을 숨김
|
|
||||||
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 <ErrorBoundary>
|
|
||||||
<AuthContextWrapper>
|
|
||||||
<BudgetProvider>
|
<BudgetProvider>
|
||||||
<Router>
|
<Router>
|
||||||
<div className="flex flex-col min-h-screen">
|
<SafeAreaContainer className="min-h-screen bg-neuro-background">
|
||||||
<div className="pt-[10px] py-[15px]"></div> {/* 상단 여백 5px에서 10px로 증가 */}
|
|
||||||
<NavBar />
|
|
||||||
<div className="flex-grow">
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Index />} />
|
<Route path="/" element={<Index />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="/profile" element={<PrivateRoute>
|
|
||||||
<ProfileManagement />
|
|
||||||
</PrivateRoute>} />
|
|
||||||
<Route path="/settings" element={<Settings />} />
|
|
||||||
{/* 지출 페이지는 더 이상 인증이 필요하지 않음 */}
|
|
||||||
<Route path="/transactions" element={<Transactions />} />
|
|
||||||
{/* 분석 페이지는 더 이상 인증이 필요하지 않음 */}
|
|
||||||
<Route path="/analytics" element={<Analytics />} />
|
|
||||||
{/* 보안 및 개인정보 페이지도 로그인 없이 접근 가능하도록 수정 */}
|
|
||||||
<Route path="/security-privacy" element={<SecurityPrivacySettings />} />
|
|
||||||
<Route path="/notifications" element={<PrivateRoute>
|
|
||||||
<NotificationSettings />
|
|
||||||
</PrivateRoute>} />
|
|
||||||
<Route path="/help-support" element={<HelpSupport />} />
|
|
||||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||||
<Route path="/payment-methods" element={<PrivateRoute>
|
<Route path="/settings" element={<Settings />} />
|
||||||
<PaymentMethods />
|
<Route path="/security-privacy" element={<SecurityPrivacySettings />} />
|
||||||
</PrivateRoute>} />
|
<Route path="/profile" element={<ProfileManagement />} />
|
||||||
|
<Route path="/payment-methods" element={<PaymentMethods />} />
|
||||||
|
<Route path="/notifications" element={<NotificationSettings />} />
|
||||||
|
<Route path="/help-support" element={<HelpSupport />} />
|
||||||
|
<Route path="/analytics" element={<Analytics />} />
|
||||||
|
<Route path="/transactions" element={<Transactions />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</SafeAreaContainer>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</div>
|
<SonnerToaster />
|
||||||
</Router>
|
</Router>
|
||||||
</BudgetProvider>
|
</BudgetProvider>
|
||||||
</AuthContextWrapper>
|
</AuthProvider>
|
||||||
</ErrorBoundary>;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAuth } from '@/contexts/auth';
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
|
import { isIOSPlatform } from '@/utils/platform';
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -14,6 +15,12 @@ const Header: React.FC = () => {
|
|||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const [isIOS, setIsIOS] = useState(false);
|
||||||
|
|
||||||
|
// 플랫폼 감지
|
||||||
|
useEffect(() => {
|
||||||
|
setIsIOS(isIOSPlatform());
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 이미지 프리로딩 처리
|
// 이미지 프리로딩 처리
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -28,16 +35,27 @@ const Header: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <header className="py-4">
|
return (
|
||||||
|
<header className={`py-4 ${isIOS ? 'ios-header' : ''}`}>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Avatar className="h-12 w-12 mr-3">
|
<Avatar className="h-12 w-12 mr-3">
|
||||||
{!imageLoaded && !imageError ? <div className="h-full w-full flex items-center justify-center">
|
{!imageLoaded && !imageError ? (
|
||||||
|
<div className="h-full w-full flex items-center justify-center">
|
||||||
<Skeleton className="h-full w-full rounded-full" />
|
<Skeleton className="h-full w-full rounded-full" />
|
||||||
</div> : <>
|
</div>
|
||||||
<AvatarImage src="/zellyy.png" alt="Zellyy" className={imageLoaded ? 'opacity-100' : 'opacity-0'} onLoad={() => setImageLoaded(true)} onError={() => setImageError(true)} />
|
) : (
|
||||||
|
<>
|
||||||
|
<AvatarImage
|
||||||
|
src="/zellyy.png"
|
||||||
|
alt="Zellyy"
|
||||||
|
className={imageLoaded ? 'opacity-100' : 'opacity-0'}
|
||||||
|
onLoad={() => setImageLoaded(true)}
|
||||||
|
onError={() => setImageError(true)}
|
||||||
|
/>
|
||||||
{(imageError || !imageLoaded) && <AvatarFallback delayMs={100}>ZY</AvatarFallback>}
|
{(imageError || !imageLoaded) && <AvatarFallback delayMs={100}>ZY</AvatarFallback>}
|
||||||
</>}
|
</>
|
||||||
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-bold neuro-text text-xl">
|
<h1 className="font-bold neuro-text text-xl">
|
||||||
@@ -50,7 +68,8 @@ const Header: React.FC = () => {
|
|||||||
<Bell size={20} className="text-gray-600" />
|
<Bell size={20} className="text-gray-600" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>;
|
</header>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|||||||
47
src/components/SafeAreaContainer.tsx
Normal file
47
src/components/SafeAreaContainer.tsx
Normal file
@@ -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<SafeAreaContainerProps> = ({
|
||||||
|
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 (
|
||||||
|
<div className={`${safeAreaClass} ${className}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SafeAreaContainer;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@@ -51,6 +52,12 @@
|
|||||||
--sidebar-border: 220 13% 91%;
|
--sidebar-border: 220 13% 91%;
|
||||||
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--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 {
|
.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;
|
@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) {
|
@media (max-width: 768px) {
|
||||||
.neuro-card {
|
.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-inter {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
src/main.tsx
22
src/main.tsx
@@ -1,5 +1,27 @@
|
|||||||
|
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import './index.css'
|
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(<App />);
|
createRoot(document.getElementById("root")!).render(<App />);
|
||||||
|
|
||||||
|
|||||||
@@ -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 => {
|
export const isAndroidPlatform = (): boolean => {
|
||||||
return getPlatform() === 'android';
|
return Capacitor.getPlatform() === 'android';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 앱이 iOS 플랫폼에서 실행 중인지 확인합니다.
|
* iOS 플랫폼인지 확인
|
||||||
*/
|
*/
|
||||||
export const isIOSPlatform = (): boolean => {
|
export const isIOSPlatform = (): boolean => {
|
||||||
return getPlatform() === 'ios';
|
return Capacitor.getPlatform() === 'ios';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 앱이 웹 플랫폼에서 실행 중인지 확인합니다.
|
* 웹 플랫폼인지 확인
|
||||||
*/
|
*/
|
||||||
export const isWebPlatform = (): boolean => {
|
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();
|
return isAndroidPlatform() || isIOSPlatform();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 앱 버전 정보를 가져옵니다.
|
* 앱 버전 정보 가져오기
|
||||||
* @returns 앱 버전 정보 객체
|
|
||||||
*/
|
*/
|
||||||
export const getAppVersionInfo = async (): Promise<{
|
export const getAppVersionInfo = async () => {
|
||||||
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('안드로이드 플랫폼 감지: 빌드 정보 가져오기 준비');
|
|
||||||
|
|
||||||
// 플러그인이 완전히 로드될 때까지 잠시 대기
|
|
||||||
await new Promise(resolve => setTimeout(resolve, INITIAL_DELAY));
|
|
||||||
|
|
||||||
// 재시도 로직을 포함한 플러그인 호출 함수
|
|
||||||
const tryGetBuildInfo = async (retryCount = 0): Promise<typeof defaultVersionInfo> => {
|
|
||||||
try {
|
try {
|
||||||
console.log(`빌드 정보 플러그인 호출 시도 (${retryCount + 1}/${MAX_RETRY + 1})...`);
|
// BuildInfoPlugin이 설치되어 있다면 사용
|
||||||
|
if (Capacitor.isPluginAvailable('BuildInfo')) {
|
||||||
// 실패한 경우 지연 시간을 늘려가며 재시도
|
const buildInfo = await Capacitor.Plugins.BuildInfo.getBuildInfo();
|
||||||
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 {
|
return {
|
||||||
versionName: nativeBuildConfig.VERSION_NAME || defaultVersionInfo.versionName,
|
versionName: buildInfo.versionName,
|
||||||
versionCode: Number(nativeBuildConfig.VERSION_CODE) || defaultVersionInfo.versionCode,
|
buildNumber: parseInt(buildInfo.buildNumber, 10),
|
||||||
buildNumber: Number(nativeBuildConfig.BUILD_NUMBER) || defaultVersionInfo.buildNumber,
|
versionCode: buildInfo.versionCode
|
||||||
|
? parseInt(buildInfo.versionCode, 10)
|
||||||
|
: undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} 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 {
|
return {
|
||||||
versionName: buildInfo.versionName || defaultVersionInfo.versionName,
|
versionName: '1.0.1',
|
||||||
versionCode: Number(buildInfo.versionCode) || defaultVersionInfo.versionCode,
|
buildNumber: 2
|
||||||
buildNumber: Number(buildInfo.buildNumber) || defaultVersionInfo.buildNumber,
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`빌드 정보 플러그인 호출 실패 (${retryCount + 1}/${MAX_RETRY + 1}):`, error);
|
console.error('앱 버전 정보를 가져오는 중 오류 발생:', error);
|
||||||
|
|
||||||
// 최대 재시도 횟수에 도달하지 않았다면 재시도
|
|
||||||
if (retryCount < MAX_RETRY) {
|
|
||||||
return tryGetBuildInfo(retryCount + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
versionName: defaultVersionInfo.versionName,
|
versionName: '1.0.1',
|
||||||
versionCode: defaultVersionInfo.versionCode,
|
buildNumber: 2
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user