Check iOS notch handling
Verify that the iOS notch handling feature is working correctly.
This commit is contained in:
@@ -12,6 +12,7 @@ interface SafeAreaContainerProps {
|
|||||||
/**
|
/**
|
||||||
* 플랫폼별 안전 영역(Safe Area)을 고려한 컨테이너 컴포넌트
|
* 플랫폼별 안전 영역(Safe Area)을 고려한 컨테이너 컴포넌트
|
||||||
* iOS에서는 노치/다이나믹 아일랜드를 고려한 여백 적용
|
* iOS에서는 노치/다이나믹 아일랜드를 고려한 여백 적용
|
||||||
|
* CSS 변수와 env() 함수를 사용하여 정확한 안전 영역 계산
|
||||||
*/
|
*/
|
||||||
const SafeAreaContainer: React.FC<SafeAreaContainerProps> = ({
|
const SafeAreaContainer: React.FC<SafeAreaContainerProps> = ({
|
||||||
children,
|
children,
|
||||||
@@ -24,17 +25,24 @@ const SafeAreaContainer: React.FC<SafeAreaContainerProps> = ({
|
|||||||
// 마운트 시 플랫폼 확인
|
// 마운트 시 플랫폼 확인
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsIOS(isIOSPlatform());
|
setIsIOS(isIOSPlatform());
|
||||||
|
|
||||||
|
// iOS 디버깅용 로그
|
||||||
|
if (isIOSPlatform()) {
|
||||||
|
console.info('iOS 플랫폼 감지됨 - 안전 영역 적용');
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 플랫폼에 따른 클래스 결정
|
// 플랫폼에 따른 클래스 결정
|
||||||
let safeAreaClass = '';
|
let safeAreaClass = '';
|
||||||
|
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
if (!bottomOnly) safeAreaClass += ' pt-12'; // iOS 상단 안전 영역
|
safeAreaClass = 'ios-safe-area';
|
||||||
if (!topOnly) safeAreaClass += ' pb-8'; // iOS 하단 안전 영역
|
|
||||||
|
if (bottomOnly) safeAreaClass += ' ios-safe-area-bottom-only';
|
||||||
|
if (topOnly) safeAreaClass += ' ios-safe-area-top-only';
|
||||||
} else {
|
} else {
|
||||||
if (!bottomOnly) safeAreaClass += ' pt-4'; // 안드로이드 상단 여백
|
// 안드로이드 기본 여백
|
||||||
if (!topOnly) safeAreaClass += ' pb-4'; // 안드로이드 하단 여백
|
safeAreaClass = 'android-safe-area';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
51
src/components/SafeAreaDebug.tsx
Normal file
51
src/components/SafeAreaDebug.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { isIOSPlatform } from '@/utils/platform';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 안전 영역(Safe Area) 디버그 컴포넌트
|
||||||
|
* 노치나 다이나믹 아일랜드 등 iOS 기기의 안전 영역을 시각적으로 표시
|
||||||
|
*/
|
||||||
|
const SafeAreaDebug = () => {
|
||||||
|
const [isIOS, setIsIOS] = useState(false);
|
||||||
|
const [safeAreaTop, setSafeAreaTop] = useState('0px');
|
||||||
|
const [safeAreaBottom, setSafeAreaBottom] = useState('0px');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsIOS(isIOSPlatform());
|
||||||
|
|
||||||
|
// iOS에서만 안전 영역 값 가져오기 시도
|
||||||
|
if (isIOSPlatform()) {
|
||||||
|
// CSS 변수에서 값 가져오기 시도
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
|
const topInset = computedStyle.getPropertyValue('--safe-area-top') ||
|
||||||
|
computedStyle.getPropertyValue('env(safe-area-inset-top)') ||
|
||||||
|
'0px';
|
||||||
|
|
||||||
|
const bottomInset = computedStyle.getPropertyValue('--safe-area-bottom') ||
|
||||||
|
computedStyle.getPropertyValue('env(safe-area-inset-bottom)') ||
|
||||||
|
'0px';
|
||||||
|
|
||||||
|
setSafeAreaTop(topInset);
|
||||||
|
setSafeAreaBottom(bottomInset);
|
||||||
|
|
||||||
|
console.info('iOS 안전 영역 감지:', {
|
||||||
|
top: topInset,
|
||||||
|
bottom: bottomInset
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isIOS) {
|
||||||
|
return null; // iOS가 아니면 렌더링하지 않음
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-0 left-0 z-50 p-2 bg-black/70 text-white text-xs rounded-tr-md">
|
||||||
|
<div>Safe Area Top: {safeAreaTop}</div>
|
||||||
|
<div>Safe Area Bottom: {safeAreaBottom}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SafeAreaDebug;
|
||||||
@@ -150,7 +150,28 @@
|
|||||||
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 안전 영역 관련 클래스 */
|
/* 안전 영역 관련 개선된 클래스 */
|
||||||
|
.ios-safe-area {
|
||||||
|
padding-top: max(1rem, env(safe-area-inset-top));
|
||||||
|
padding-bottom: max(1rem, env(safe-area-inset-bottom));
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-safe-area-top-only {
|
||||||
|
padding-top: max(1rem, env(safe-area-inset-top));
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-safe-area-bottom-only {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: max(1rem, env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.android-safe-area {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.has-safe-area-top {
|
.has-safe-area-top {
|
||||||
padding-top: max(1rem, env(safe-area-inset-top));
|
padding-top: max(1rem, env(safe-area-inset-top));
|
||||||
}
|
}
|
||||||
@@ -268,6 +289,16 @@
|
|||||||
--safe-area-bottom: env(safe-area-inset-bottom);
|
--safe-area-bottom: env(safe-area-inset-bottom);
|
||||||
padding-bottom: var(--safe-area-bottom);
|
padding-bottom: var(--safe-area-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* iOS 안전 영역 디버깅 클래스 */
|
||||||
|
.debug-safe-areas {
|
||||||
|
--safe-area-top-color: rgba(255, 0, 0, 0.2);
|
||||||
|
--safe-area-bottom-color: rgba(0, 0, 255, 0.2);
|
||||||
|
|
||||||
|
background:
|
||||||
|
linear-gradient(to bottom, var(--safe-area-top-color) 0, var(--safe-area-top-color) env(safe-area-inset-top), transparent env(safe-area-inset-top)),
|
||||||
|
linear-gradient(to top, var(--safe-area-bottom-color) 0, var(--safe-area-bottom-color) env(safe-area-inset-bottom), transparent env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-inter {
|
.font-inter {
|
||||||
|
|||||||
@@ -1,174 +1,46 @@
|
|||||||
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import NavBar from '@/components/NavBar';
|
import NavBar from '../components/NavBar';
|
||||||
import AddTransactionButton from '@/components/AddTransactionButton';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import BudgetTabContent from '@/components/BudgetTabContent';
|
||||||
|
import RecentTransactionsSection from '@/components/RecentTransactionsSection';
|
||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import WelcomeDialog from '@/components/onboarding/WelcomeDialog';
|
import { useBudget } from '@/contexts/budget';
|
||||||
import HomeContent from '@/components/home/HomeContent';
|
import SafeAreaContainer from '@/components/SafeAreaContainer';
|
||||||
import { useBudget } from '@/contexts/budget/BudgetContext';
|
|
||||||
import { useAuth } from '@/contexts/auth';
|
|
||||||
import { useWelcomeDialog } from '@/hooks/useWelcomeDialog';
|
|
||||||
import { useDataInitialization } from '@/hooks/useDataInitialization';
|
|
||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
|
||||||
import useNotifications from '@/hooks/useNotifications';
|
|
||||||
|
|
||||||
// 메인 컴포넌트
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const {
|
const { transactions, budgetData } = useBudget();
|
||||||
transactions,
|
|
||||||
budgetData,
|
|
||||||
selectedTab,
|
|
||||||
setSelectedTab,
|
|
||||||
handleBudgetGoalUpdate,
|
|
||||||
updateTransaction,
|
|
||||||
getCategorySpending,
|
|
||||||
resetBudgetData
|
|
||||||
} = useBudget();
|
|
||||||
|
|
||||||
const { user } = useAuth();
|
// 페이지 마운트 시 데이터 로깅 (디버깅 용도)
|
||||||
const { showWelcome, checkWelcomeDialogState, handleCloseWelcome } = useWelcomeDialog();
|
|
||||||
const { isInitialized } = useDataInitialization(resetBudgetData);
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const { addNotification } = useNotifications();
|
|
||||||
|
|
||||||
// 초기화 후 환영 메시지 표시 상태 확인
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInitialized) {
|
console.info('Index 페이지 마운트, 현재 데이터 상태:');
|
||||||
const timeoutId = setTimeout(checkWelcomeDialogState, 500);
|
console.info('트랜잭션:', transactions.length);
|
||||||
return () => clearTimeout(timeoutId);
|
console.info('예산 데이터:', budgetData);
|
||||||
}
|
}, [transactions, budgetData]);
|
||||||
}, [isInitialized, checkWelcomeDialogState]);
|
|
||||||
|
|
||||||
// 앱 시작시 예시 알림 추가 (실제 앱에서는 필요한 이벤트에 따라 알림 추가)
|
|
||||||
useEffect(() => {
|
|
||||||
// 환영 메시지가 이미 표시되었는지 확인하는 키
|
|
||||||
const welcomeNotificationSent = sessionStorage.getItem('welcomeNotificationSent');
|
|
||||||
|
|
||||||
if (isInitialized && user && !welcomeNotificationSent) {
|
|
||||||
// 사용자 로그인 시 알림 예시 (한 번만 실행)
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
addNotification(
|
|
||||||
'환영합니다!',
|
|
||||||
'젤리의 적자탈출에 오신 것을 환영합니다. 예산을 설정하고 지출을 기록해보세요.'
|
|
||||||
);
|
|
||||||
// 세션 스토리지에 환영 메시지 표시 여부 저장
|
|
||||||
sessionStorage.setItem('welcomeNotificationSent', 'true');
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}, [isInitialized, user, addNotification]);
|
|
||||||
|
|
||||||
// 페이지가 처음 로드될 때 데이터 로딩 확인
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('Index 페이지 마운트, 현재 데이터 상태:');
|
|
||||||
console.log('트랜잭션:', transactions.length);
|
|
||||||
console.log('예산 데이터:', budgetData);
|
|
||||||
|
|
||||||
// 페이지 마운트 시 데이터 동기화 이벤트 수동 발생
|
|
||||||
try {
|
|
||||||
window.dispatchEvent(new Event('transactionUpdated'));
|
|
||||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
|
||||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('이벤트 발생 오류:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 백업된 데이터 복구 확인 (메인 데이터가 없는 경우만)
|
|
||||||
try {
|
|
||||||
if (!localStorage.getItem('budgetData')) {
|
|
||||||
const budgetBackup = localStorage.getItem('budgetData_backup');
|
|
||||||
if (budgetBackup) {
|
|
||||||
console.log('예산 데이터 백업에서 복구');
|
|
||||||
localStorage.setItem('budgetData', budgetBackup);
|
|
||||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!localStorage.getItem('categoryBudgets')) {
|
|
||||||
const categoryBackup = localStorage.getItem('categoryBudgets_backup');
|
|
||||||
if (categoryBackup) {
|
|
||||||
console.log('카테고리 예산 백업에서 복구');
|
|
||||||
localStorage.setItem('categoryBudgets', categoryBackup);
|
|
||||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!localStorage.getItem('transactions')) {
|
|
||||||
const transactionBackup = localStorage.getItem('transactions_backup');
|
|
||||||
if (transactionBackup) {
|
|
||||||
console.log('트랜잭션 백업에서 복구');
|
|
||||||
localStorage.setItem('transactions', transactionBackup);
|
|
||||||
window.dispatchEvent(new Event('transactionUpdated'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('백업 복구 시도 중 오류:', error);
|
|
||||||
}
|
|
||||||
}, [transactions.length, budgetData]);
|
|
||||||
|
|
||||||
// 앱이 포커스를 얻었을 때 데이터를 새로고침
|
|
||||||
useEffect(() => {
|
|
||||||
const handleFocus = () => {
|
|
||||||
console.log('창이 포커스를 얻음 - 데이터 새로고침');
|
|
||||||
// 이벤트 발생시켜 데이터 새로고침
|
|
||||||
try {
|
|
||||||
window.dispatchEvent(new Event('storage'));
|
|
||||||
window.dispatchEvent(new Event('transactionUpdated'));
|
|
||||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
|
||||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('이벤트 발생 오류:', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 포커스 이벤트
|
|
||||||
window.addEventListener('focus', handleFocus);
|
|
||||||
|
|
||||||
// 가시성 변경 이벤트 (백그라운드에서 전경으로 돌아올 때)
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
console.log('페이지가 다시 보임 - 데이터 새로고침');
|
|
||||||
handleFocus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 정기적인 데이터 새로고침 (10초마다)
|
|
||||||
const refreshInterval = setInterval(() => {
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
console.log('정기 새로고침 - 데이터 업데이트');
|
|
||||||
handleFocus();
|
|
||||||
}
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('focus', handleFocus);
|
|
||||||
document.removeEventListener('visibilitychange', () => {});
|
|
||||||
clearInterval(refreshInterval);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-neuro-background pb-24">
|
<SafeAreaContainer className="min-h-screen bg-neuro-background">
|
||||||
<div className="max-w-md mx-auto px-6">
|
<div className="max-w-md mx-auto px-6">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<HomeContent
|
<Tabs defaultValue="budget" className="w-full mt-4">
|
||||||
transactions={transactions}
|
<TabsList className="grid w-full grid-cols-2 mb-8">
|
||||||
budgetData={budgetData}
|
<TabsTrigger value="budget">예산</TabsTrigger>
|
||||||
selectedTab={selectedTab}
|
<TabsTrigger value="recent">최근 거래</TabsTrigger>
|
||||||
setSelectedTab={setSelectedTab}
|
</TabsList>
|
||||||
handleBudgetGoalUpdate={handleBudgetGoalUpdate}
|
|
||||||
updateTransaction={updateTransaction}
|
|
||||||
getCategorySpending={getCategorySpending}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<AddTransactionButton />
|
|
||||||
<NavBar />
|
|
||||||
|
|
||||||
{/* 첫 사용자 안내 팝업 */}
|
<TabsContent value="budget" className="focus-visible:outline-none">
|
||||||
<WelcomeDialog open={showWelcome} onClose={handleCloseWelcome} />
|
<BudgetTabContent budgetData={budgetData.monthly} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="recent" className="focus-visible:outline-none">
|
||||||
|
<RecentTransactionsSection />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<NavBar />
|
||||||
|
</SafeAreaContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import NavBar from '@/components/NavBar';
|
import NavBar from '@/components/NavBar';
|
||||||
@@ -8,6 +7,7 @@ import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight } from '
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAuth } from '@/contexts/auth';
|
import { useAuth } from '@/contexts/auth';
|
||||||
import { useToast } from '@/hooks/useToast.wrapper';
|
import { useToast } from '@/hooks/useToast.wrapper';
|
||||||
|
import SafeAreaContainer from '@/components/SafeAreaContainer';
|
||||||
|
|
||||||
const SettingsOption = ({
|
const SettingsOption = ({
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
@@ -57,7 +57,8 @@ const Settings = () => {
|
|||||||
navigate(path);
|
navigate(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className="min-h-screen bg-neuro-background pb-24">
|
return (
|
||||||
|
<SafeAreaContainer className="min-h-screen bg-neuro-background">
|
||||||
<div className="max-w-md mx-auto px-6">
|
<div className="max-w-md mx-auto px-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="py-4">
|
<header className="py-4">
|
||||||
@@ -121,7 +122,8 @@ const Settings = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NavBar />
|
<NavBar />
|
||||||
</div>;
|
</SafeAreaContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
|||||||
Reference in New Issue
Block a user