Fix: SafeArea for iOS dynamic island

Fixes iOS safe area issues caused by the dynamic island.
This commit is contained in:
gpt-engineer-app[bot]
2025-04-05 05:08:29 +00:00
parent af51ba2d52
commit b7b55cc816
4 changed files with 106 additions and 17 deletions

View File

@@ -21,7 +21,17 @@ const Header: React.FC = () => {
// 플랫폼 감지 // 플랫폼 감지
useEffect(() => { useEffect(() => {
setIsIOS(isIOSPlatform()); const checkPlatform = async () => {
try {
const isiOS = isIOSPlatform();
console.log('Header: iOS 플랫폼 감지 결과:', isiOS);
setIsIOS(isiOS);
} catch (error) {
console.error('플랫폼 감지 중 오류:', error);
}
};
checkPlatform();
}, []); }, []);
// 이미지 프리로딩 처리 // 이미지 프리로딩 처리
@@ -37,7 +47,8 @@ const Header: React.FC = () => {
}; };
}, []); }, []);
const headerClass = isIOS ? 'has-safe-area-top' : 'py-4'; // iOS 전용 헤더 클래스 - 안전 영역 적용
const headerClass = isIOS ? 'ios-notch-padding' : 'py-4';
return ( return (
<header className={headerClass}> <header className={headerClass}>

View File

@@ -25,15 +25,22 @@ const SafeAreaContainer: React.FC<SafeAreaContainerProps> = ({
// 마운트 시 플랫폼 확인 // 마운트 시 플랫폼 확인
useEffect(() => { useEffect(() => {
setIsIOS(isIOSPlatform()); const checkPlatform = async () => {
const isiOS = isIOSPlatform();
console.log('SafeAreaContainer: 플랫폼 확인 - iOS:', isiOS);
setIsIOS(isiOS);
};
checkPlatform();
}, []); }, []);
// 플랫폼에 따른 클래스 결정 // 플랫폼에 따른 클래스 결정
let safeAreaClass = ''; let safeAreaClass = 'safe-area-container';
if (isIOS) { if (isIOS) {
if (!bottomOnly) safeAreaClass += ' has-safe-area-top'; // iOS 상단 안전 영역 if (!bottomOnly) safeAreaClass += ' has-safe-area-top'; // iOS 상단 안전 영역
if (!topOnly) safeAreaClass += ' has-safe-area-bottom'; // iOS 하단 안전 영역 if (!topOnly) safeAreaClass += ' has-safe-area-bottom'; // iOS 하단 안전 영역
safeAreaClass += ' ios-safe-area'; // iOS 전용 클래스 추가
} else { } else {
if (!bottomOnly) safeAreaClass += ' pt-4'; // 안드로이드 상단 여백 if (!bottomOnly) safeAreaClass += ' pt-4'; // 안드로이드 상단 여백
if (!topOnly) safeAreaClass += ' pb-4'; // 안드로이드 하단 여백 if (!topOnly) safeAreaClass += ' pb-4'; // 안드로이드 하단 여백
@@ -42,6 +49,30 @@ const SafeAreaContainer: React.FC<SafeAreaContainerProps> = ({
// 추가 하단 여백 적용 // 추가 하단 여백 적용
const extraBottomClass = extraBottomPadding ? 'pb-[80px]' : ''; const extraBottomClass = extraBottomPadding ? 'pb-[80px]' : '';
// 디버그용 로그 추가
useEffect(() => {
if (isIOS) {
console.log('SafeAreaContainer: iOS 안전 영역 적용됨', {
topOnly,
bottomOnly,
extraBottomPadding
});
// 안전 영역 값 확인 (CSS 변수)
try {
const computedStyle = getComputedStyle(document.documentElement);
console.log('Safe area 변수 값:', {
top: computedStyle.getPropertyValue('--safe-area-top'),
bottom: computedStyle.getPropertyValue('--safe-area-bottom'),
left: computedStyle.getPropertyValue('--safe-area-left'),
right: computedStyle.getPropertyValue('--safe-area-right')
});
} catch (error) {
console.error('CSS 변수 확인 중 오류:', error);
}
}
}, [isIOS, topOnly, bottomOnly]);
return ( return (
<div className={`${safeAreaClass} ${extraBottomClass} ${className}`}> <div className={`${safeAreaClass} ${extraBottomClass} ${className}`}>
{children} {children}

View File

@@ -52,11 +52,11 @@
--sidebar-ring: 217.2 91.2% 59.8%; --sidebar-ring: 217.2 91.2% 59.8%;
/* Safe area 값 */ /* Safe area 값 - !important 추가하여 우선 적용 */
--safe-area-top: env(safe-area-inset-top, 0px); --safe-area-top: env(safe-area-inset-top, 0px) !important;
--safe-area-bottom: env(safe-area-inset-bottom, 0px); --safe-area-bottom: env(safe-area-inset-bottom, 0px) !important;
--safe-area-left: env(safe-area-inset-left, 0px); --safe-area-left: env(safe-area-inset-left, 0px) !important;
--safe-area-right: env(safe-area-inset-right, 0px); --safe-area-right: env(safe-area-inset-right, 0px) !important;
} }
.dark { .dark {
@@ -150,17 +150,29 @@
@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;
} }
/* 안전 영역 관련 클래스 */ /* 안전 영역 관련 클래스 - 강화된 버전 */
.safe-area-container {
width: 100%;
position: relative;
box-sizing: border-box;
}
.ios-safe-area {
/* iOS 전용 안전 영역 처리 */
position: relative;
box-sizing: border-box;
}
.has-safe-area-top { .has-safe-area-top {
padding-top: max(1rem, var(--safe-area-top)); padding-top: max(1rem, var(--safe-area-top)) !important;
} }
.has-safe-area-bottom { .has-safe-area-bottom {
padding-bottom: max(1rem, var(--safe-area-bottom)); padding-bottom: max(1rem, var(--safe-area-bottom)) !important;
} }
.ios-header { .ios-header {
padding-top: max(1rem, var(--safe-area-top)); padding-top: max(1rem, var(--safe-area-top)) !important;
} }
/* 모바일 화면에서의 추가 스타일 */ /* 모바일 화면에서의 추가 스타일 */
@@ -186,6 +198,11 @@
.SheetContent { .SheetContent {
@apply rounded-xl overflow-hidden; @apply rounded-xl overflow-hidden;
} }
/* iOS 고유 노치/다이나믹 아일랜드 영역 고려한 추가 스타일 */
.ios-safe-area-screen {
padding: var(--safe-area-top) var(--safe-area-right) var(--safe-area-bottom) var(--safe-area-left) !important;
}
} }
/* 데스크탑 화면에서의 추가 스타일 */ /* 데스크탑 화면에서의 추가 스타일 */
@@ -260,11 +277,21 @@
/* iOS 전용 스타일 */ /* iOS 전용 스타일 */
@supports (-webkit-touch-callout: none) { @supports (-webkit-touch-callout: none) {
.ios-safe-area-top { .ios-safe-area-top {
padding-top: var(--safe-area-top); padding-top: var(--safe-area-top) !important;
} }
.ios-safe-area-bottom { .ios-safe-area-bottom {
padding-bottom: var(--safe-area-bottom); padding-bottom: var(--safe-area-bottom) !important;
}
/* iOS에서 노치/다이나믹 아일랜드 영역 처리를 위한 추가 클래스 */
.ios-notch-padding {
padding-top: max(1.5rem, var(--safe-area-top)) !important;
}
/* iOS에서 하단 홈 인디케이터 영역을 위한 추가 클래스 */
.ios-bottom-padding {
padding-bottom: max(1.5rem, var(--safe-area-bottom)) !important;
} }
} }

View File

@@ -12,6 +12,26 @@ import { useDataInitialization } from '@/hooks/useDataInitialization';
import { useIsMobile } from '@/hooks/use-mobile'; import { useIsMobile } from '@/hooks/use-mobile';
import useNotifications from '@/hooks/useNotifications'; import useNotifications from '@/hooks/useNotifications';
import SafeAreaContainer from '@/components/SafeAreaContainer'; import SafeAreaContainer from '@/components/SafeAreaContainer';
import { BudgetData } from '@/contexts/budget/types';
// 기본 예산 데이터 (빈 객체 대신 사용할 더미 데이터)
const defaultBudgetData: BudgetData = {
daily: {
targetAmount: 0,
spentAmount: 0,
remainingAmount: 0
},
weekly: {
targetAmount: 0,
spentAmount: 0,
remainingAmount: 0
},
monthly: {
targetAmount: 0,
spentAmount: 0,
remainingAmount: 0
}
};
// 메인 컴포넌트 // 메인 컴포넌트
const Index = () => { const Index = () => {
@@ -69,7 +89,7 @@ const Index = () => {
try { try {
console.log('Index 페이지 마운트, 현재 데이터 상태:'); console.log('Index 페이지 마운트, 현재 데이터 상태:');
console.log('트랜잭션:', transactions?.length || 0); console.log('트랜잭션:', transactions?.length || 0);
console.log('예산 데이터:', budgetData); console.log('예산 데이터:', budgetData || defaultBudgetData);
// 페이지 첫 마운트 시에만 실행되는 로직으로 수정 // 페이지 첫 마운트 시에만 실행되는 로직으로 수정
const isFirstMount = sessionStorage.getItem('initialDataLoaded') !== 'true'; const isFirstMount = sessionStorage.getItem('initialDataLoaded') !== 'true';
@@ -194,7 +214,7 @@ const Index = () => {
<HomeContent <HomeContent
transactions={transactions || []} transactions={transactions || []}
budgetData={budgetData || {}} budgetData={budgetData || defaultBudgetData}
selectedTab={selectedTab} selectedTab={selectedTab}
setSelectedTab={setSelectedTab} setSelectedTab={setSelectedTab}
handleBudgetGoalUpdate={handleBudgetGoalUpdate} handleBudgetGoalUpdate={handleBudgetGoalUpdate}