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-
-
-
-

-
-
+
+ {/* Next.js Image 대신 일반 img 태그 사용 */}
+
+

+ handleImageError(e, '모임 스타일에 맞는 모임 개설')
+ }
+ />
+
+
+
모임 공개 여부
-
-
-
- {isMobile ? (
-
- ) : (
-
- )}
-
+
+
+ {typeof isMobile !== 'undefined' &&
+ (isMobile ? (
+
+ ) : (
+
+ ))}
+
모임 생성이 완료 되었습니다!