diff --git a/public/basketball.png b/public/basketball.png new file mode 100644 index 0000000..d2dc73e Binary files /dev/null and b/public/basketball.png differ diff --git a/public/beige_cat.png b/public/beige_cat.png new file mode 100644 index 0000000..86896fb Binary files /dev/null and b/public/beige_cat.png differ diff --git a/public/front_notebook.png b/public/front_notebook.png new file mode 100644 index 0000000..2f8c238 Binary files /dev/null and b/public/front_notebook.png differ diff --git a/public/gray_cat.png b/public/gray_cat.png new file mode 100644 index 0000000..bbb8bf9 Binary files /dev/null and b/public/gray_cat.png differ diff --git a/public/headset.png b/public/headset.png new file mode 100644 index 0000000..f5da23f Binary files /dev/null and b/public/headset.png differ diff --git a/public/heart_notebook.png b/public/heart_notebook.png new file mode 100644 index 0000000..1df092b Binary files /dev/null and b/public/heart_notebook.png differ diff --git a/public/read_cat.png b/public/read_cat.png new file mode 100644 index 0000000..75298d9 Binary files /dev/null and b/public/read_cat.png differ diff --git a/public/white_cat.png b/public/white_cat.png new file mode 100644 index 0000000..f2c46ae Binary files /dev/null and b/public/white_cat.png differ diff --git a/public/white_notebook_cat.png b/public/white_notebook_cat.png new file mode 100644 index 0000000..c7644be Binary files /dev/null and b/public/white_notebook_cat.png differ diff --git a/public/yellow_cat.png b/public/yellow_cat.png new file mode 100644 index 0000000..d8b897f Binary files /dev/null and b/public/yellow_cat.png differ diff --git a/src/app/page.tsx b/src/app/page.tsx index 82c500e..fb272f5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,10 @@ 'use client'; -import Section1Image from '@/assets/images/section1.png'; -import Section2Image from '@/assets/images/section2.png'; import { Button } from '@/components/ui/Button'; import { motion } from 'framer-motion'; import dynamic from 'next/dynamic'; -import Image from 'next/image'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +// import Image from 'next/image'; // Next.js Image 컴포넌트 제거 +import { useCallback, useEffect, useRef, useState } from 'react'; import DevingLogo from '../assets/icon/devingLogo.svg'; import GithubIcon from '../assets/icon/github_icon.svg'; @@ -15,7 +13,7 @@ import MediumCheckIcon from '../assets/icon/medium_check_icon.svg'; import NotionIcon from '../assets/icon/notion_icon.svg'; import SmallCheckIcon from '../assets/icon/small_check_Icon.svg'; -// 타입 정의 - 라이브러리에서 필요한 속성 정확하게 지정 +// FullPage 타입 정의 interface FullPageProps { credits: { enabled: boolean; @@ -34,18 +32,53 @@ interface FullPageProps { render: (props: { fullpageApi?: unknown }) => JSX.Element; } -// 클라이언트 사이드에서만 ReactFullpage 로드 +const FallbackFullPage: React.FC = () =>
; +FallbackFullPage.displayName = 'FallbackFullPage'; + +// LoadingFullPage 수정 - 텍스트 제거 +const LoadingFullPage: React.FC = () =>
; +LoadingFullPage.displayName = 'LoadingFullPage'; + const FullPage = dynamic( - () => import('@fullpage/react-fullpage').then((mod) => mod.default), - { ssr: false }, + () => + import('@fullpage/react-fullpage') + .then((mod) => mod.default) + .catch((err) => { + console.error('FullPage 로드 실패:', err); + return FallbackFullPage; + }), + { + ssr: false, + loading: () => , + }, ); +// Fallback 컴포넌트를 미리 정의하여 displayName 설정 +const FallbackWrapper: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + return
{children}
; +}; +FallbackWrapper.displayName = 'FallbackWrapper'; + +const LoadingWrapper: React.FC = () =>
; +LoadingWrapper.displayName = 'LoadingWrapper'; + const FullPageWrapper = dynamic( - () => import('@fullpage/react-fullpage').then((mod) => mod.default.Wrapper), - { ssr: false }, + () => + import('@fullpage/react-fullpage') + .then((mod) => mod.default.Wrapper) + .catch((err) => { + console.error('FullPageWrapper 로드 실패:', err); + return FallbackWrapper; + }), + { + ssr: false, + loading: () => , + }, ); -// 애니메이션 설정을 객체로 분리하여 관리 +// 애니메이션 설정 const animations = { fadeInUp: { hidden: { opacity: 0, y: 60 }, @@ -86,18 +119,23 @@ const animations = { }, }; -// 언마운트 오류를 방지하기 위한 래퍼 컴포넌트 +// 개선된 오류 처리 및 정리 기능이 있는 래퍼 컴포넌트 const SafeFullPage: React.FC = (props) => { const [mounted, setMounted] = useState(false); + const styleRef = useRef(null); + const originalConsoleErrorRef = useRef(null); useEffect(() => { - setMounted(true); + // 클라이언트 환경에 완전히 있는지 확인하기 위해 다음 틱까지 대기 + const timer = setTimeout(() => { + setMounted(true); + }, 0); - // 콘솔 경고 필터링을 위한 원본 콘솔 참조 저장 - const originalConsoleError = console.error; + // 원본 console.error 저장 + originalConsoleErrorRef.current = console.error; - // fullPage 라이센스 경고 필터링 함수 - const filterConsoleError = (...args: Parameters) => { + // fullPage 라이센스 경고를 위한 콘솔 필터링 + console.error = (...args: Parameters) => { if ( typeof args[0] === 'string' && (args[0].includes('fullPage:') || @@ -106,43 +144,49 @@ const SafeFullPage: React.FC = (props) => { ) { return; // fullPage 라이센스 메시지 무시 } - originalConsoleError(...args); + // 다른 오류에 대해 원본 console.error 호출 + if (originalConsoleErrorRef.current) { + originalConsoleErrorRef.current(...args); + } }; - // 콘솔 오류 필터링 적용 - console.error = filterConsoleError; - - // 스타일 요소 생성 및 적용 - const style = document.createElement('style'); - style.innerHTML = ` - .fp-watermark, #fp-nav ul li .fp-tooltip, .fp-warning { - display: none !important; - } + // fullPage 사용자 정의를 위한 스타일 요소 생성 + styleRef.current = document.createElement('style'); + if (styleRef.current) { + styleRef.current.innerHTML = ` + .fp-watermark, #fp-nav ul li .fp-tooltip, .fp-warning { + display: none !important; + } - /* 첫 번째 섹션 스크롤 제거 */ - #fp-nav + .fp-section:first-of-type { - overflow: hidden !important; - } - - /* fp-auto-height 섹션이 올바르게 표시되도록 스타일 조정 */ - .fp-auto-height.fp-section { - height: auto !important; - } - - .fp-auto-height .fp-tableCell { - height: auto !important; - padding-bottom: 0 !important; - } - `; - document.head.appendChild(style); + /* 첫 번째 섹션 스크롤 제거 */ + #fp-nav + .fp-section:first-of-type { + overflow: hidden !important; + } + + /* 자동 높이 섹션 스타일 */ + .fp-auto-height.fp-section { + height: auto !important; + } + + .fp-auto-height .fp-tableCell { + height: auto !important; + padding-bottom: 0 !important; + } + `; + document.head.appendChild(styleRef.current); + } return () => { + clearTimeout(timer); setMounted(false); - // 원래의 console.error 복원 - console.error = originalConsoleError; - // 언마운트 전에 정리를 위한 지연 - setTimeout(() => { + // 원본 console.error 복원 + if (originalConsoleErrorRef.current) { + console.error = originalConsoleErrorRef.current; + } + + // 더 안전한 DOM 조작으로 정리 함수 + try { const fpNav = document.querySelector('#fp-nav'); if (fpNav) fpNav.remove(); @@ -151,83 +195,127 @@ const SafeFullPage: React.FC = (props) => { section.classList.remove('active', 'fp-section', 'fp-table'); }); - if (document.head.contains(style)) { - document.head.removeChild(style); + // 스타일 요소 제거 + if (styleRef.current && document.head.contains(styleRef.current)) { + document.head.removeChild(styleRef.current); } - }, 0); + } catch (error) { + console.error('정리 오류:', error); + } }; }, []); - if (!mounted) return null; + // SSR 문제 방지를 위해 마운트된 경우에만 FullPage 렌더링 + if (!mounted) { + return ( +
+ ); + } - return ; + // 오류 처리를 추가하기 위해 props.render 래핑 + const safeRender = (renderProps: { fullpageApi?: unknown }) => { + try { + return props.render(renderProps); + } catch (error) { + console.error('렌더링 오류:', error); + return
; + } + }; + + return ; +}; + +// 이미지 로드 오류 처리 함수 +const handleImageError = ( + event: React.SyntheticEvent, + altText: string, +) => { + const imgElement = event.currentTarget; + imgElement.onerror = null; // 무한 루프 방지 + + // 데이터 URI를 사용하여 SVG 자리 표시자 생성 + const width = imgElement.width || 300; + const height = imgElement.height || 200; + + imgElement.src = `data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"%3E%3Crect width="${width}" height="${height}" fill="%23f0f0f0"/%3E%3Ctext x="50%25" y="50%25" dominant-baseline="middle" text-anchor="middle" font-family="sans-serif" font-size="14" fill="%23999"%3E${altText || '이미지 없음'}%3C/text%3E%3C/svg%3E`; }; export default function Home() { const [isClient, setIsClient] = useState(false); const [currentSection, setCurrentSection] = useState(0); const [isMobile, setIsMobile] = useState(false); + const [fullPageLoaded, setFullPageLoaded] = useState(false); - // 모바일 감지 함수를 useCallback으로 최적화 + // 디바운스가 있는 모바일 감지 const checkMobile = useCallback(() => { - setIsMobile(window.innerWidth < 768); + if (typeof window !== 'undefined') { + setIsMobile(window.innerWidth < 768); + } }, []); - // afterLoad 핸들러를 useCallback으로 최적화 + // 더 안전한 afterLoad 핸들러 const handleAfterLoad = useCallback( (destination: { index: number }): void => { - setCurrentSection(destination.index); + if (destination && typeof destination.index === 'number') { + setCurrentSection(destination.index); + } }, [], ); - // 동적 모듈 사전 로드 함수 + // 더 나은 오류 처리로 모듈 사전 로드 const preloadModules = useCallback(async () => { try { await import('@fullpage/react-fullpage'); - // console.log 대신 주석으로 처리 - // console.log('FullPage 모듈 로드 성공'); + setFullPageLoaded(true); } catch (error) { console.error('FullPage 모듈 로드 실패:', error); } }, []); useEffect(() => { - // 컴포넌트 마운트 즉시 상태 변경 - setIsClient(true); - - // 초기 모바일 체크 - checkMobile(); - - // 리사이즈 이벤트에 대응 - window.addEventListener('resize', checkMobile); - - // 모듈 사전 로드 - preloadModules(); + // 정리로 메모리 누수 방지 + let mounted = true; + + // 안전한 클라이언트 측 감지 + if (typeof window !== 'undefined' && mounted) { + setIsClient(true); + checkMobile(); + + // 디바운스된 리사이즈 리스너 추가 + let resizeTimer: NodeJS.Timeout; + const handleResize = () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + if (mounted) checkMobile(); + }, 250); + }; + + window.addEventListener('resize', handleResize); + + // 모듈 사전 로드 + preloadModules(); + + return () => { + mounted = false; + clearTimeout(resizeTimer); + window.removeEventListener('resize', handleResize); + }; + } return () => { - // 이벤트 리스너 정리 - window.removeEventListener('resize', checkMobile); + mounted = false; }; }, [checkMobile, preloadModules]); - // 로딩 상태일 때 표시될 컴포넌트 - if (!isClient) { - return ( -
- 로딩 중... - Loading... -
- ); + if (!isClient || !fullPageLoaded) { + return
; } return ( { + render={({ fullpageApi }) => { return (
- {/* 메인 히어로 섹션 */} -
+ {/* 히어로 섹션 */} +
-
+ {/* Next.js Image 대신 일반 img 태그 사용 */} +
+ 고양이 + 고양이 + 고양이 + 고양이 + 고양이 + 고양이 + 고양이 + + 고양이 + 고양이 + 고양이 +
- {/* 섹션 1: 성장하는 모임 */} + {/* 섹션 1: 성장하는 커뮤니티 */}
-
- 코드만큼 성장하는 모임 -
-
+
+ {/* Next.js Image 대신 일반 img 태그 사용 */} +
+ 코드만큼 성장하는 모임 + handleImageError(e, '코드만큼 성장하는 모임') + } + /> +
+
+
모임 시작
-
+
D-
-
+
7
-
+
-
- 모임 스타일에 맞는 모임 개설 -
-
+
+ {/* Next.js Image 대신 일반 img 태그 사용 */} +
+ 모임 스타일에 맞는 모임 개설 + handleImageError(e, '모임 스타일에 맞는 모임 개설') + } + /> +
+
+
모임 공개 여부
-
+
비공개
-
-
- {isMobile ? ( -