diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 8f39513..474be41 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,9 @@ const Header: React.FC = () => { const [imageLoaded, setImageLoaded] = useState(false); const [imageError, setImageError] = useState(false); const isMobile = useIsMobile(); + + // iOS 기기에서는 상단 안전 영역을 위한 클래스 적용 + const safeAreaClass = isIOSPlatform() ? 'needs-top-safe-area' : ''; // 이미지 프리로딩 처리 useEffect(() => { @@ -28,16 +32,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 +65,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..53aee63 --- /dev/null +++ b/src/components/SafeAreaContainer.tsx @@ -0,0 +1,33 @@ + +import React from 'react'; +import { isIOSPlatform } from '@/utils/platform'; + +interface SafeAreaContainerProps { + children: React.ReactNode; + className?: string; + applyTop?: boolean; + applyBottom?: boolean; +} + +/** + * 안전 영역(Safe Area)을 고려한 컨테이너 컴포넌트 + * iOS의 다이나믹 아일랜드/노치와 하단 홈 인디케이터를 고려하여 패딩을 적용합니다. + */ +const SafeAreaContainer: React.FC = ({ + children, + className = '', + applyTop = true, + applyBottom = false +}) => { + // iOS 플랫폼인 경우에만 안전 영역 패딩 적용 + const topPadding = applyTop && isIOSPlatform() ? 'pt-safe-area' : ''; + const bottomPadding = applyBottom && isIOSPlatform() ? 'pb-safe-area' : ''; + + return ( +
+ {children} +
+ ); +}; + +export default SafeAreaContainer; diff --git a/src/index.css b/src/index.css index 3a6b7ce..e6e075f 100644 --- a/src/index.css +++ b/src/index.css @@ -1,190 +1,84 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); - @tailwind base; @tailwind components; @tailwind utilities; +/* Define the glassmorphism effect */ +.glass { + background: rgba(255, 255, 255, 0.2); + border-radius: 16px; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.3); +} +/* Define the neumorphism effect */ +.neuro { + background: #E0E5EC; + border-radius: 10px; + box-shadow: -5px -5px 10px #FFFFFF, + 5px 5px 10px #BABECC; +} +/* Neumorphism text */ +.neuro-text { + color: #666; + text-shadow: -1px -1px 1px #fff, 1px 1px 1px #babebc; +} +/* Flat neumorphism effect (pressed) */ +.neuro-flat { + border-radius: 10px; + background: #E0E5EC; + box-shadow: inset -5px -5px 10px #FFFFFF, + inset 5px 5px 10px #BABECC; +} +/* Skeleton styles */ +.skeleton { + animation: skeleton-loading 1.2s linear infinite alternate; +} -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - - --radius: 1rem; - - --sidebar-background: 0 0% 98%; - - --sidebar-foreground: 240 5.3% 26.1%; - - --sidebar-primary: 240 5.9% 10%; - - --sidebar-primary-foreground: 0 0% 98%; - - --sidebar-accent: 240 4.8% 95.9%; - - --sidebar-accent-foreground: 240 5.9% 10%; - - --sidebar-border: 220 13% 91%; - - --sidebar-ring: 217.2 91.2% 59.8%; +@keyframes skeleton-loading { + 0% { + background-color: hsl(200, 20%, 70%); } - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; + 100% { + background-color: hsl(200, 20%, 95%); } } -@layer base { - * { - @apply border-border; - } - - body { - @apply bg-neuro-background text-foreground font-inter antialiased; - } - - html, body, #root { - @apply h-full overflow-x-hidden; - } +/* 안전 영역 (Safe Area) 관련 유틸리티 클래스 */ +:root { + --safe-area-top: 0px; + --safe-area-bottom: 0px; } -@layer components { - .neuro-flat { - @apply bg-neuro-background shadow-neuro-flat rounded-xl; - } - - .neuro-pressed { - @apply bg-neuro-background shadow-neuro-pressed rounded-xl; - } - - .neuro-convex { - @apply bg-neuro-background shadow-neuro-convex rounded-xl; - } - - .neuro-text { - @apply font-medium tracking-wide; - } - - .page-transition-enter { - @apply animate-fade-in; - } - - .glass-effect { - @apply bg-white/10 backdrop-blur-lg border border-white/20 rounded-xl; - } - - .neuro-button { - @apply neuro-flat px-4 py-3 text-neuro-accent font-medium transition-all duration-200 - hover:shadow-neuro-convex hover:text-neuro-accent-light active:shadow-neuro-pressed; - } - - .neuro-card { - @apply neuro-flat p-6 transition-all duration-300 hover:shadow-neuro-convex; - } - - .neuro-input { - @apply neuro-pressed px-4 py-3 w-full focus:outline-none focus:ring-2 focus:ring-neuro-accent/30; - } - - /* 모바일 화면에서의 추가 스타일 */ - @media (max-width: 768px) { - .neuro-card { - @apply w-full; - } - - #root { - @apply p-0; - } - - /* 모바일에서 팝업과 다이얼로그 스타일 보정 */ - [role="dialog"] { - @apply rounded-xl overflow-hidden; - } - - /* 다이얼로그 내용에 적용되는 스타일 */ - .DialogContent, - .PopoverContent, - .AlertDialogContent, - .DrawerContent, - .SheetContent { - @apply rounded-xl overflow-hidden; - } - } - - /* 데스크탑 화면에서의 추가 스타일 */ - @media (min-width: 769px) { - #root { - @apply px-0; - } - - .desktop-container { - @apply max-w-md mx-auto; - } - - .desktop-card { - @apply w-full mx-auto; - } - } +.pt-safe-area { + padding-top: var(--safe-area-top); } -.font-inter { - font-family: 'Inter', sans-serif; +.pb-safe-area { + padding-bottom: var(--safe-area-bottom); +} + +.ios-platform .needs-top-safe-area { + padding-top: var(--safe-area-top); +} + +.ios-platform .needs-bottom-safe-area { + padding-bottom: var(--safe-area-bottom); +} + +/* 추가 플랫폼별 스타일 */ +.ios-platform-only { + display: none; +} + +.android-platform-only { + display: none; +} + +.ios-platform .ios-platform-only { + display: block; +} + +.android-platform .android-platform-only { + display: block; } diff --git a/src/main.tsx b/src/main.tsx index 719464e..853d380 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,14 @@ -import { createRoot } from 'react-dom/client' -import App from './App.tsx' +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' import './index.css' +import { applySafeAreaInsets } from './utils/safeArea'; -createRoot(document.getElementById("root")!).render(); +// 안전 영역 CSS 변수 초기화 +applySafeAreaInsets(); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/utils/safeArea.ts b/src/utils/safeArea.ts new file mode 100644 index 0000000..520bffd --- /dev/null +++ b/src/utils/safeArea.ts @@ -0,0 +1,61 @@ + +import { isAndroidPlatform, isIOSPlatform } from './platform'; + +/** + * 기기 플랫폼에 따라 상단 안전 영역(Safe Area) 패딩 값을 반환합니다. + * iOS의 경우 다이나믹 아일랜드/노치를 위한 여백을 제공합니다. + */ +export const getTopSafeAreaPadding = (): string => { + if (isIOSPlatform()) { + return 'env(safe-area-inset-top, 47px)'; + } + return '0px'; +}; + +/** + * 기기 플랫폼에 따라 하단 안전 영역(Safe Area) 패딩 값을 반환합니다. + * iOS의 경우 홈 인디케이터를 위한 여백을 제공합니다. + */ +export const getBottomSafeAreaPadding = (): string => { + if (isIOSPlatform()) { + return 'env(safe-area-inset-bottom, 34px)'; + } + return '0px'; +}; + +/** + * 플랫폼 감지 및 안전 영역 CSS 변수를 문서에 적용합니다. + * 이 함수는 앱 초기화시 한 번 호출해야 합니다. + */ +export const applySafeAreaInsets = (): void => { + const root = document.documentElement; + + // 최초 로드 시 안전 영역 값 설정 + updateSafeAreaVariables(); + + // 방향 변경 시 안전 영역 값 다시 계산 + window.addEventListener('orientationchange', updateSafeAreaVariables); + window.addEventListener('resize', updateSafeAreaVariables); +}; + +/** + * 안전 영역 CSS 변수를 문서에 업데이트합니다. + */ +const updateSafeAreaVariables = (): void => { + const root = document.documentElement; + + // 플랫폼에 따른 안전 영역 패딩 계산 + root.style.setProperty('--safe-area-top', getTopSafeAreaPadding()); + root.style.setProperty('--safe-area-bottom', getBottomSafeAreaPadding()); + + // 디버깅용: 현재 사용 중인 플랫폼 표시 + if (isIOSPlatform()) { + root.classList.add('ios-platform'); + root.classList.remove('android-platform'); + } else if (isAndroidPlatform()) { + root.classList.add('android-platform'); + root.classList.remove('ios-platform'); + } else { + root.classList.remove('ios-platform', 'android-platform'); + } +};