diff --git a/README.md b/README.md index e69de29..0c8136b 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,169 @@ +# SSOUL 프로젝트 인수인계 문서 + +## �� 프로젝트 개요 + +**프로젝트명**: SSOUL (칵테일을 좋아하는 사람들을 위한 서비스) +**기술 스택**: Next.js 15, React 19, TypeScript, Tailwind CSS +**저장소**: https://github.com/prgrms-web-devcourse-final-project/WEB5_6_HaeDokCoding_FE + +## �� 시작하기 + +### 개발 환경 설정 +```bash +# 의존성 설치 +npm install + +# 개발 서버 실행 +npm run dev + +# 빌드 +npm run build + +# 코드 포맷팅 +npm run format + +# 린트 검사 +npm run lint +``` + +### 환경 변수 +프로젝트는 개발/운영 환경에 따라 다른 API URL을 사용합니다: +- `NEXT_PUBLIC_API_URL_DEV`: 개발 환경 API URL +- `NEXT_PUBLIC_API_URL_PROD`: 운영 환경 API URL + +## �� 프로젝트 구조 + +``` +src/ +├── app/ # Next.js App Router 페이지 +│ ├── layout.tsx # 루트 레이아웃 +│ ├── page.tsx # 메인 페이지 +│ ├── community/ # 커뮤니티 관련 페이지 +│ ├── recipe/ # 칵테일 레시피 페이지 +│ ├── recommend/ # 취향 추천 페이지 +│ ├── login/ # 로그인 관련 페이지 +│ ├── mypage/ # 마이페이지 +│ └── api/ # API 설정 +├── domains/ # 도메인별 컴포넌트 +│ ├── community/ # 커뮤니티 도메인 +│ ├── recipe/ # 레시피 도메인 +│ ├── recommend/ # 추천 도메인 +│ ├── login/ # 로그인 도메인 +│ ├── mypage/ # 마이페이지 도메인 +│ └── shared/ # 공통 도메인 컴포넌트 +└── shared/ # 공통 컴포넌트 및 유틸 + ├── components/ # 공통 UI 컴포넌트 + ├── styles/ # 글로벌 스타일 + ├── assets/ # 이미지, 아이콘 등 + ├── hook/ # 공통 훅 + ├── types/ # 타입 정의 + └── utills/ # 유틸리티 함수 +``` + +## �� 주요 기능 + +### 1. 인증 시스템 +- **소셜 로그인**: Google, Kakao, Naver 지원 +- **상태 관리**: Zustand + localStorage persist +- **주요 파일**: + - `src/domains/shared/store/auth.ts`: 인증 상태 관리 + - `src/domains/login/hook/useAuthHooks.ts`: 로그인 관련 훅 + - `src/app/api/config/appConfig.ts`: API 설정 + +### 2. 페이지별 기능 + +#### �� 메인 페이지 (`/`) +- 현재 기본 구조만 구현됨 +- 추후 확장 예정 + +#### �� 칵테일 레시피 (`/recipe`) +- **주요 컴포넌트**: + - `CocktailList`: 칵테일 목록 표시 + - `Accordion`: 필터링 옵션 + - `SelectBox`: 정렬 옵션 +- **기능**: 검색, 필터링, 정렬 +- **상세 페이지**: `/recipe/[id]` - 개별 칵테일 상세 정보 + +#### �� 커뮤니티 (`/community`) +- **주요 컴포넌트**: + - `PostCard`: 게시물 카드 + - `CommunityTab`: 카테고리 탭 + - `WriteBtn`: 글쓰기 버튼 +- **카테고리**: 레시피, 팁, 질문, 자유 +- **글쓰기**: `/community/write` +- **상세 페이지**: `/community/[id]` + +#### �� 취향 추천 (`/recommend`) +- **챗봇 기반 추천**: `ChatSection` 컴포넌트 +- **주요 컴포넌트**: + - `BotMessage`, `UserMessage`: 메시지 컴포넌트 + - `BotOptions`: 선택 옵션 + - `MessageInput`: 입력창 + - `TypingIndicator`: 타이핑 효과 + +#### �� 마이페이지 (`/mypage`) +- **기본 리다이렉트**: `/mypage` → `/mypage/mybar` +- **주요 섹션**: + - `/mypage/mybar`: 나만의 바 + - `/mypage/my-active`: 활동 내역 (게시물, 댓글, 좋아요) + - `/mypage/my-alarm`: 알림 설정 + - `/mypage/my-setting`: 계정 설정 + +#### �� 로그인 (`/login`) +- **소셜 로그인**: `SocialLogin` 컴포넌트 +- **성공 페이지**: `/login/success` +- **신규 사용자**: `/login/user/first-user` + +## �� 기술적 세부사항 + +### 상태 관리 +- **Zustand**: 클라이언트 상태 관리 +- **Persist**: localStorage를 통한 상태 영속화 +- **주요 스토어**: + - `auth.ts`: 사용자 인증 상태 + - `accordionStore.ts`: 아코디언 UI 상태 + +### UI/UX +- **Tailwind CSS**: 스타일링 +- **React Hot Toast**: 토스트 알림 +- **Lottie**: 로딩 애니메이션 +- **GSAP**: 고급 애니메이션 +- **Responsive**: 모바일/데스크톱 대응 + +### 개발 도구 +- **ESLint**: 코드 품질 관리 +- **Prettier**: 코드 포맷팅 +- **Husky**: Git hooks +- **Lint-staged**: 커밋 전 검사 + +## �� 주요 설정 파일 + +- `next.config.ts`: Next.js 설정 +- `tailwind.config.js`: Tailwind CSS 설정 +- `eslint.config.mjs`: ESLint 설정 +- `tsconfig.json`: TypeScript 설정 + +## �� 반응형 디자인 + +프로젝트는 모바일 우선(Mobile-first) 접근 방식을 사용합니다: +- **모바일**: 기본 스타일 +- **태블릿**: `md:` prefix +- **데스크톱**: `lg:`, `xl:` prefix + +## �� 주의사항 + +1. **환경 변수**: 개발/운영 환경에 맞는 API URL 설정 필요 +2. **인증 토큰**: localStorage에 저장되므로 보안 고려 필요 +3. **API 통신**: `credentials: 'include'` 설정으로 쿠키 기반 인증 +4. **Git Hooks**: Husky 설정으로 커밋 전 자동 검사 + +## �� 추가 문의 + +- **저장소 이슈**: https://github.com/prgrms-web-devcourse-final-project/WEB5_6_HaeDokCoding_FE/issues +- **주요 브랜치**: `main` (메인), `dev` (개발) + +--- + +**작성일**: 2025-10-14 +**작성자**: 이성헌 +**버전**: 1.0 \ No newline at end of file diff --git a/public/fonts/NanumSquareNeo-aLt.ttf b/public/fonts/NanumSquareNeo-aLt.ttf new file mode 100644 index 0000000..5946187 Binary files /dev/null and b/public/fonts/NanumSquareNeo-aLt.ttf differ diff --git a/public/fonts/NanumSquareNeo-bRg.ttf b/public/fonts/NanumSquareNeo-bRg.ttf new file mode 100644 index 0000000..8680fb3 Binary files /dev/null and b/public/fonts/NanumSquareNeo-bRg.ttf differ diff --git a/public/fonts/NanumSquareNeo-cBd.ttf b/public/fonts/NanumSquareNeo-cBd.ttf new file mode 100644 index 0000000..ccde7f7 Binary files /dev/null and b/public/fonts/NanumSquareNeo-cBd.ttf differ diff --git a/public/fonts/NanumSquareNeo-dEb.ttf b/public/fonts/NanumSquareNeo-dEb.ttf new file mode 100644 index 0000000..0f3b2a8 Binary files /dev/null and b/public/fonts/NanumSquareNeo-dEb.ttf differ diff --git a/public/fonts/NanumSquareNeo-eHv.ttf b/public/fonts/NanumSquareNeo-eHv.ttf new file mode 100644 index 0000000..51a02ab Binary files /dev/null and b/public/fonts/NanumSquareNeo-eHv.ttf differ diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 79bf3c1..c91b392 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -1,12 +1,13 @@ -import FooterWrapper from '@/shared/components/footer/FooterWrapper'; import Header from '@/shared/components/header/Header'; function NoLayout({ children }: { children: React.ReactNode }) { return ( <> -
+
{children}
- ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5edb3bd..5c6374c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,7 @@ +import '@/shared/styles/global.css'; import type { Metadata } from 'next'; import '@/shared/styles/global.css'; import { Toaster } from 'react-hot-toast'; -import Header from '@/shared/components/header/Header'; -import FooterWrapper from '@/shared/components/footer/FooterWrapper'; import ScrollTopBtnWrapper from '@/shared/components/scroll-top/ScrollTopBtnWrapper'; import KaKaoScript from './api/kakao/KaKaoScript'; import 'swiper/css'; diff --git a/src/domains/login/hook/useLoginRedirect.ts b/src/domains/login/hook/useLoginRedirect.ts index 70d1d74..7c2b2f1 100644 --- a/src/domains/login/hook/useLoginRedirect.ts +++ b/src/domains/login/hook/useLoginRedirect.ts @@ -1,5 +1,5 @@ import { useAuthStore } from '@/domains/shared/store/auth'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { usePathname, useRouter } from 'next/navigation'; import { getCookie, removeCookie } from '@/domains/shared/auth/utils/cookie'; import { useToast } from '@/shared/hook/useToast'; @@ -12,6 +12,7 @@ export const useLoginRedirect = () => { const [loading, setLoading] = useState(true); const [welcomeModalOpen, setWelcomeModalOpen] = useState(false); + const hasShownToast = useRef(false); useEffect(() => { if (!user && loading) { @@ -39,7 +40,8 @@ export const useLoginRedirect = () => { if (pathname.startsWith('/login/user/first-user')) { setWelcomeModalOpen(true); - } else if (pathname.startsWith('/login/user/success')) { + } else if (pathname.startsWith('/login/user/success') && !hasShownToast.current) { + hasShownToast.current = true; toastSuccess(`${user.nickname}님 \n 로그인 성공 🎉`); router.replace(preLoginPath); setTimeout(() => removeCookie('preLoginPath'), 500); diff --git a/src/domains/main/cocktailDrop/CocktailDrop.tsx b/src/domains/main/cocktailDrop/CocktailDrop.tsx index 216f994..8c399ea 100644 --- a/src/domains/main/cocktailDrop/CocktailDrop.tsx +++ b/src/domains/main/cocktailDrop/CocktailDrop.tsx @@ -44,11 +44,23 @@ function CocktailDrop({ isDesktop = false }: CocktailDropProps) { ); // 로고 위에서 아래로 자연스럽게 등장 + const screenWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const isTablet = screenWidth >= 640 && screenWidth < 1024; + const isMobile = screenWidth < 640; + + // 뷰포트 높이 기반으로 로고 위치 계산 + const logoFinalY = isMobile + ? `-${viewportHeight * 0.3}px` + : isTablet + ? `-${viewportHeight * -0.8}px` + : '0px'; + gsap.fromTo( logoRef.current, { y: -300, opacity: 0 }, { - y: !isDesktop ? -230 : -18, // 데스크톱이 아닐 때 더 위로 + y: logoFinalY, // 뷰포트 높이 기반 계산 opacity: 1, duration: 3, ease: 'power3.out', @@ -69,7 +81,7 @@ function CocktailDrop({ isDesktop = false }: CocktailDropProps) { return (
{/* 대각선 줄 1 */} @@ -84,7 +96,7 @@ function CocktailDrop({ isDesktop = false }: CocktailDropProps) { /> {/* 로고 */} -
+
로고 이미지
-
- {/* 컵 이미지 - 모바일에서 바닥에 붙도록 */} -
+
칵테일 컵
-
+
diff --git a/src/domains/main/components/3d/HomeLogo.tsx b/src/domains/main/components/3d/HomeLogo.tsx index b80a870..6ad77a4 100644 --- a/src/domains/main/components/3d/HomeLogo.tsx +++ b/src/domains/main/components/3d/HomeLogo.tsx @@ -3,8 +3,8 @@ import Image from 'next/image'; function HomeLogo({ isDesktop }: { isDesktop: boolean }) { return (
로고 이미지
diff --git a/src/domains/main/components/3d/HomeModel.tsx b/src/domains/main/components/3d/HomeModel.tsx index de0eac9..0e9e821 100644 --- a/src/domains/main/components/3d/HomeModel.tsx +++ b/src/domains/main/components/3d/HomeModel.tsx @@ -42,8 +42,8 @@ function Model({ onLoaded }: Props) { return ( ); diff --git a/src/domains/main/components/3d/HomeText.tsx b/src/domains/main/components/3d/HomeText.tsx index 64ec5f4..0a3fab9 100644 --- a/src/domains/main/components/3d/HomeText.tsx +++ b/src/domains/main/components/3d/HomeText.tsx @@ -2,8 +2,9 @@ function HomeText({ isDesktop }: { isDesktop: boolean }) { return ( <> {!isDesktop ? ( -

- 어떤 칵테일이 끌리시나요? SSoul이 쉽게 골라드릴게요. +

+ 어떤 칵테일이 끌리시나요?
+ SSoul이 쉽게 골라드릴게요.

) : (

diff --git a/src/domains/main/components/3d/Landing.tsx b/src/domains/main/components/3d/Landing.tsx index 4f480cf..7f82a17 100644 --- a/src/domains/main/components/3d/Landing.tsx +++ b/src/domains/main/components/3d/Landing.tsx @@ -3,7 +3,6 @@ import HomeModel from './HomeModel'; import HomeLogo from './HomeLogo'; import HomeText from './HomeText'; -import Scroll from './Scroll'; import { useEffect, useState } from 'react'; import ModelImage from './ModelImage'; @@ -30,7 +29,6 @@ function Landing({ setIsLoading, isDesktop }: Props) { <> - )}

diff --git a/src/domains/main/components/3d/ModelImage.tsx b/src/domains/main/components/3d/ModelImage.tsx index f6a4ddf..d556432 100644 --- a/src/domains/main/components/3d/ModelImage.tsx +++ b/src/domains/main/components/3d/ModelImage.tsx @@ -19,7 +19,7 @@ function ModelImage({ onLoaded }: Props) { width={260} height={290} priority - className="object-cover w-[300px] h-[390px]" + className="object-cover w-[300px] h-[350px]" />
diff --git a/src/domains/main/components/3d/Scroll.tsx b/src/domains/main/components/3d/Scroll.tsx index 45dcc58..2264d5e 100644 --- a/src/domains/main/components/3d/Scroll.tsx +++ b/src/domains/main/components/3d/Scroll.tsx @@ -1,22 +1,30 @@ import Lottie from 'lottie-react'; import scroll from '@/shared/assets/lottie/ScrollDownAnimation.json'; -function Scroll({ isDesktop }: { isDesktop: boolean }) { - const style = !isDesktop - ? { - width: 45, - height: 45, - } - : { - width: 60, - height: 60, - }; +type Props = { + ref: React.RefObject; +}; + +function Scroll({ ref }: Props) { + const style = { + width: 50, + height: 50, + }; return ( -
-
- -
-
+ ); } diff --git a/src/domains/main/components/3d/StarMain.tsx b/src/domains/main/components/3d/StarMain.tsx index 5f18189..d212331 100644 --- a/src/domains/main/components/3d/StarMain.tsx +++ b/src/domains/main/components/3d/StarMain.tsx @@ -14,6 +14,10 @@ function StarMain() { useEffect(() => { if (!background.current || !foreground.current) return; + // 모바일에서는 별 애니메이션 비활성화 + const isMobile = window.innerWidth < 768; + if (isMobile) return; + const bgX = gsap.quickSetter(background.current, 'x', 'px'); const bgY = gsap.quickSetter(background.current, 'y', 'px'); const bgRotate = gsap.quickSetter(background.current, 'rotate', 'deg'); @@ -43,20 +47,11 @@ function StarMain() { mouse.current = { x, y }; }; - const handleTouchMove = (e: TouchEvent) => { - const touch = e.touches[0]; - const x = (touch.clientX / window.innerWidth - 0.5) * 2; - const y = (touch.clientY / window.innerHeight - 0.5) * 2; - mouse.current = { x, y }; - }; - window.addEventListener('mousemove', handleMouseMove); - window.addEventListener('touchmove', handleTouchMove); rafId.current = requestAnimationFrame(update); return () => { window.removeEventListener('mousemove', handleMouseMove); - window.removeEventListener('touchmove', handleTouchMove); if (rafId.current) cancelAnimationFrame(rafId.current); }; }, []); diff --git a/src/domains/main/components/FinalLanding.tsx b/src/domains/main/components/FinalLanding.tsx index 4416a7f..7add66c 100644 --- a/src/domains/main/components/FinalLanding.tsx +++ b/src/domains/main/components/FinalLanding.tsx @@ -7,6 +7,7 @@ import gsap from 'gsap'; import { ScrollSmoother, ScrollTrigger } from 'gsap/all'; import StarMain from './3d/StarMain'; import CocktailDrop from '../cocktailDrop/CocktailDrop'; +import Scroll from './3d/Scroll'; function FinalLanding() { const [isLoading, setIsLoading] = useState(true); @@ -15,6 +16,9 @@ function FinalLanding() { const [isDesktop, setIsDesktop] = useState(false); const [hasMounted, setHasMounted] = useState(false); + const scrollRef = useRef(null); + const [showScrollBtn, setShowScrollBtn] = useState(false); + useEffect(() => { const checkViewport = () => { setIsDesktop(window.innerWidth >= 1024); @@ -60,6 +64,40 @@ function FinalLanding() { }; }, [isDesktop]); + // scroll 버튼 + useEffect(() => { + if (!scrollRef.current) return; + + const tl = gsap.to(scrollRef.current, { + y: 12, + repeat: -1, + yoyo: true, + duration: 0.8, + ease: 'power1.inOut', + }); + + return () => { + tl.kill(); + }; + }, [showScrollBtn]); + + useEffect(() => { + const updateScrollBtn = () => { + const scrollTop = window.scrollY || document.documentElement.scrollTop; + const maxScroll = document.documentElement.scrollHeight - window.innerHeight; + setShowScrollBtn(scrollTop < maxScroll - 5); + }; + + updateScrollBtn(); // 초기 체크 + window.addEventListener('scroll', updateScrollBtn); + window.addEventListener('resize', updateScrollBtn); + + return () => { + window.removeEventListener('scroll', updateScrollBtn); + window.removeEventListener('resize', updateScrollBtn); + }; + }, []); + if (!hasMounted) return null; return ( @@ -90,6 +128,7 @@ function FinalLanding() { )} )} + {showScrollBtn && } ); } diff --git a/src/domains/main/components/mainSlide/components/MainSlide.tsx b/src/domains/main/components/mainSlide/components/MainSlide.tsx index eba27a4..d9f12b3 100644 --- a/src/domains/main/components/mainSlide/components/MainSlide.tsx +++ b/src/domains/main/components/mainSlide/components/MainSlide.tsx @@ -65,7 +65,7 @@ function MainSlide({ isDesktop }: { isDesktop: boolean }) { c, { x: () => stageW() - contentW(), - duration: 2, + duration: 1, immediateRender: false, onStart: () => c.classList.remove('invisible'), }, diff --git a/src/domains/main/components/mainSlide/components/mobile/MobileSlideCommunity.tsx b/src/domains/main/components/mainSlide/components/mobile/MobileSlideCommunity.tsx index ec308f6..b8756ab 100644 --- a/src/domains/main/components/mainSlide/components/mobile/MobileSlideCommunity.tsx +++ b/src/domains/main/components/mainSlide/components/mobile/MobileSlideCommunity.tsx @@ -41,6 +41,7 @@ function MobileSlideCommunity() { src={Community01} alt="" fill + sizes="300px" priority className="object-contain object-left-bottom" /> @@ -50,6 +51,7 @@ function MobileSlideCommunity() { src={Community02} alt="" fill + sizes="600px" priority className="object-contain object-left-bottom" /> diff --git a/src/domains/main/components/mainSlide/components/pc/MainSlideAbv.tsx b/src/domains/main/components/mainSlide/components/pc/MainSlideAbv.tsx index d4ed1bf..70b90ad 100644 --- a/src/domains/main/components/mainSlide/components/pc/MainSlideAbv.tsx +++ b/src/domains/main/components/mainSlide/components/pc/MainSlideAbv.tsx @@ -42,12 +42,12 @@ function MainSlideAbv() { return (
-
+

3

-
-
+
+

내 알콜도수 UP

-

+

5도 부터 시작하는 내 알콜도수
내 참여에 따라 알콜도수 UP!
알콜도수에 따라 변하는 쑤리(SSURY)를 보는 재미도 있어요.

diff --git a/src/domains/main/components/mainSlide/components/pc/MainSlideCommunity.tsx b/src/domains/main/components/mainSlide/components/pc/MainSlideCommunity.tsx index 84cc670..1795cbf 100644 --- a/src/domains/main/components/mainSlide/components/pc/MainSlideCommunity.tsx +++ b/src/domains/main/components/mainSlide/components/pc/MainSlideCommunity.tsx @@ -5,36 +5,35 @@ import Community02 from '@/shared/assets/images/community_02.webp'; function MainSlideCommunity() { return (
-
+

2

-
-

술술 즐기는, 커뮤니티

-

- 칵테일에 대해 물어볼 곳이 없어 목이 마른 당신!
- 초보자부터 애호가까지 +

+

함께 나누는 칵테일 이야기

+

+ 다양한 칵테일 레시피들을 SNS로 공유하고
- Ssoul에서는 누구나 칵테일 이야기를 나눌 수 있어요. -
- 회원들과 소통하면 내 칵테일 솜씨를 뽐내보세요. + 커뮤니티에서 누구나 칵테일 관련 이야기를 나눌 수 있어요.

-
-
+
+
-
+
diff --git a/src/domains/main/components/mainSlide/components/pc/MainSlideTest.tsx b/src/domains/main/components/mainSlide/components/pc/MainSlideTest.tsx index 7d71872..64c559e 100644 --- a/src/domains/main/components/mainSlide/components/pc/MainSlideTest.tsx +++ b/src/domains/main/components/mainSlide/components/pc/MainSlideTest.tsx @@ -16,10 +16,10 @@ const DUMMY_TEST = [ function MainSlideTest() { return (
-
+

1

-
+

AI기반 취향테스트

복잡한 이름과 긴 설명 때문에 내 취향 칵테일 찾기 어려우셨나요?
diff --git a/src/domains/mypage/components/pages/my-active/MyLike.tsx b/src/domains/mypage/components/pages/my-active/MyLike.tsx index 5f28325..216abd4 100644 --- a/src/domains/mypage/components/pages/my-active/MyLike.tsx +++ b/src/domains/mypage/components/pages/my-active/MyLike.tsx @@ -20,6 +20,10 @@ function MyLike() { fetchLike(); }, []); + useEffect(() => { + console.log(myLike); + }, [myLike]); + return (

{myLike.length > 0 ? ( diff --git a/src/domains/shared/components/3d/HomeBackground.tsx b/src/domains/shared/components/3d/HomeBackground.tsx deleted file mode 100644 index 0f45736..0000000 --- a/src/domains/shared/components/3d/HomeBackground.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useRef } from 'react'; - -function HomeBackground() { - const bgRef = useRef(null); - - useEffect(() => { - const handleMouseMove = (e: MouseEvent) => { - const x = e.clientX / window.innerWidth; - const percentage = 6 + x * 70; - if (bgRef.current) { - bgRef.current.style.background = `linear-gradient(128deg, rgba(26, 26, 26, 0.7) ${percentage}%, rgba(42, 42, 42, 0.3) ${percentage + 10}%, rgba(60, 70, 78, 0) 100%)`; - } - }; - - window.addEventListener('mousemove', handleMouseMove); - return () => window.removeEventListener('mousemove', handleMouseMove); - }, []); - - return ( -
- ); -} - -export default HomeBackground; diff --git a/src/domains/shared/components/3d/HomeModel.tsx b/src/domains/shared/components/3d/HomeModel.tsx deleted file mode 100644 index 4d0f3bc..0000000 --- a/src/domains/shared/components/3d/HomeModel.tsx +++ /dev/null @@ -1,96 +0,0 @@ -'use client'; - -import { Environment, OrbitControls, useGLTF } from '@react-three/drei'; -import { Canvas, useFrame, useThree } from '@react-three/fiber'; -import { Bloom, EffectComposer } from '@react-three/postprocessing'; -import { useEffect, useRef, useState } from 'react'; -import * as THREE from 'three'; - -function Model({ onLoaded }: { onLoaded: () => void }) { - const { scene } = useGLTF('/3d/model/scene.gltf'); - const [scale, setScale] = useState(13); - - useEffect(() => { - const isMobile = window.innerWidth < 768; // 모바일 기준 너비 - setScale(isMobile ? 3.8 : 11.5); // 모바일이면 작게 - }, []); - - useEffect(() => { - if (scene) { - onLoaded(); // 모델이 로드되면 부모에게 알림 - } - }, [scene]); - - if (!scene) return null; // 로딩 전 대기 처리 - - scene.traverse((child) => { - if ((child as THREE.Mesh).isMesh) { - const mesh = child as THREE.Mesh; - const material = mesh.material as THREE.MeshPhysicalMaterial; - - material.envMapIntensity = 3; - material.metalness = 1; - material.roughness = 0.3; - material.emissiveIntensity = 2; - material.clearcoat = 1; - material.clearcoatRoughness = 0.2; - material.needsUpdate = true; - material.opacity = 0.35; - material.bumpScale = 0.3; - material.thickness = 0.1; - } - }); - - return ; -} - -function CameraAnimation() { - const { camera } = useThree(); - const targetPosition = new THREE.Vector3(5, 10, 10); // 최종 위치 - const startPosition = new THREE.Vector3(0, 15, 6); // 시작 위치 - const progress = useRef(0); - - useFrame((state, delta) => { - if (progress.current < 1) { - progress.current += delta / 5; // 3초 동안 - const t = Math.min(progress.current, 1); - camera.position.lerpVectors(startPosition, targetPosition, t); - } - }); - - return null; -} - -function HomeModel({ onLoaded }: { onLoaded: () => void }) { - return ( - - - - - - - - - - - - - - - ); -} - -export default HomeModel; diff --git a/src/domains/shared/components/3d/Landing.tsx b/src/domains/shared/components/3d/Landing.tsx deleted file mode 100644 index ceded71..0000000 --- a/src/domains/shared/components/3d/Landing.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import HomeModel from './HomeModel'; -import StarMain from './StarMain'; -import Spinner from '@/shared/components/spinner/Spinner'; - -function Landing() { - const [isLoading, setIsLoading] = useState(true); - - return ( - <> - {isLoading && } -
-
- setIsLoading(false)} /> - {!isLoading && } -
-
- - ); -} - -export default Landing; diff --git a/src/domains/shared/components/3d/StarMain.tsx b/src/domains/shared/components/3d/StarMain.tsx deleted file mode 100644 index 87a00d9..0000000 --- a/src/domains/shared/components/3d/StarMain.tsx +++ /dev/null @@ -1,102 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import foreStar from '../../../../../public/1Stars.png'; -import backStar from '../../../../../public/2Stars.png'; -import { useEffect, useRef } from 'react'; -import gsap from 'gsap'; - -function StarMain() { - const background = useRef(null); - const foreground = useRef(null); - const mouse = useRef({ x: 0, y: 0 }); - const rafId = useRef(null); - - useEffect(() => { - if (!background.current || !foreground.current) return; - - const bgX = gsap.quickSetter(background.current, 'x', 'px'); - const bgY = gsap.quickSetter(background.current, 'y', 'px'); - const bgRotate = gsap.quickSetter(background.current, 'rotate', 'deg'); - - const fgX = gsap.quickSetter(foreground.current, 'x', 'px'); - const fgY = gsap.quickSetter(foreground.current, 'y', 'px'); - const fgRotate = gsap.quickSetter(foreground.current, 'rotate', 'deg'); - - const update = () => { - const { x, y } = mouse.current; - - bgX(x * -2); - bgY(y * -2); - bgRotate(x * -0.2); - - fgX(x * 3); - fgY(y * 3); - fgRotate(y * 0.2); - - rafId.current = requestAnimationFrame(update); - }; - - const handleMouseMove = (e: MouseEvent) => { - // 화면 중앙 기준으로 얼마나 벗어났는지 (-1 ~ 1 범위) - const x = (e.clientX / window.innerWidth - 0.5) * 2; - const y = (e.clientY / window.innerHeight - 0.5) * 2; - mouse.current = { x, y }; - }; - - const handleTouchMove = (e: TouchEvent) => { - const touch = e.touches[0]; - const x = (touch.clientX / window.innerWidth - 0.5) * 2; - const y = (touch.clientY / window.innerHeight - 0.5) * 2; - mouse.current = { x, y }; - }; - - window.addEventListener('mousemove', handleMouseMove); - window.addEventListener('touchmove', handleTouchMove); - rafId.current = requestAnimationFrame(update); - - return () => { - window.removeEventListener('mousemove', handleMouseMove); - window.removeEventListener('touchmove', handleTouchMove); - if (rafId.current) cancelAnimationFrame(rafId.current); - }; - }, []); - - return ( - <> - {/* */} -
-
-
- 앞쪽 별 -
-
- 뒤쪽 별 -
-
-
- - ); -} - -export default StarMain; diff --git a/src/shared/components/header/DropdownMenu.tsx b/src/shared/components/header/DropdownMenu.tsx index 45c31ab..3e531fb 100644 --- a/src/shared/components/header/DropdownMenu.tsx +++ b/src/shared/components/header/DropdownMenu.tsx @@ -58,6 +58,7 @@ function DropdownMenu({ isClicked, setIsClicked, visible, setVisible }: Props) { } if (isClicked) { + window.scrollTo({ top: 0, behavior: 'smooth' }); setVisible(true); tlRef.current.play(); } else { diff --git a/src/shared/components/header/Header.tsx b/src/shared/components/header/Header.tsx index 164ae3b..b389d95 100644 --- a/src/shared/components/header/Header.tsx +++ b/src/shared/components/header/Header.tsx @@ -9,11 +9,17 @@ import { useEffect, useState } from 'react'; import tw from '@/shared/utills/tw'; import { useWindowScroll } from 'react-use'; -function Header({ className }: { className?: string }) { +interface Props { + className?: string; + isMain?: boolean; +} + +function Header({ className, isMain = false }: Props) { const pathname = usePathname(); const [showShadow, setShowShadow] = useState(true); const [lastScrollTop, setLastScrollTop] = useState(0); // 마지막 스크롤 위치 저장 const [visible, setVisible] = useState(true); + const [scrollDirection, setScrollDirection] = useState<'up' | 'down' | 'none'>('none'); const { y } = useWindowScroll(); @@ -25,10 +31,19 @@ function Header({ className }: { className?: string }) { // 유저가 아래로 스크롤 -> 헤더 숨기기 setVisible(false); setShowShadow(false); + setScrollDirection('down'); } else if (diff < 0) { // 유저가 위로 스크롤 -> 헤더 다시 보이기 setVisible(true); setShowShadow(true); + setScrollDirection('up'); + } + + if (y <= 10) { + setVisible(true); + setShowShadow(false); + setScrollDirection('none'); + return; } if (y <= 10) { @@ -44,12 +59,16 @@ function Header({ className }: { className?: string }) {
10 ? 'bg-primary!' : 'bg-transparent') : 'bg-primary', + // 메인 페이지에서는 위로 스크롤할 때만 쉐도우 적용 + isMain + ? y > 10 && scrollDirection === 'up' + ? 'shadow-header' + : '' + : showShadow && 'shadow-header', className - ? className - : [ - 'bg-primary w-full h-[44px] md:h-[60px] flex items-center justify-between px-[12px] fixed top-0 left-0 z-50 transition-transform duration-200 ease-in-out', - showShadow && 'shadow-header', - ] )} > diff --git a/src/shared/styles/global.css b/src/shared/styles/global.css index 1ea41c4..2c35df5 100644 --- a/src/shared/styles/global.css +++ b/src/shared/styles/global.css @@ -5,14 +5,42 @@ @import './_components.css'; /* 1) 로컬 폰트 등록 */ + @font-face { - font-family: 'NanumSquareNeo Var'; - src: url('/fonts/NanumSquareNeo-Variable.ttf') format('truetype'); - font-weight: 100 900; + font-family: 'NanumSquareNeo'; + src: url('/fonts/NanumSquareNeo-aLt.ttf') format('truetype'); + font-weight: 300; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'NanumSquareNeo'; + src: url('/fonts/NanumSquareNeo-bRg.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'NanumSquareNeo'; + src: url('/fonts/NanumSquareNeo-cBd.ttf') format('truetype'); + font-weight: 500; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'NanumSquareNeo'; + src: url('/fonts/NanumSquareNeo-dEb.ttf') format('truetype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'NanumSquareNeo'; + src: url('/fonts/NanumSquareNeo-eHv.ttf') format('truetype'); + font-weight: 900; font-style: normal; font-display: swap; } - @font-face { font-family: 'Hahmlet'; src: url('/fonts/Hahmlet-Light.ttf') format('truetype'); font-weight: 300; font-style: normal; font-display: swap; } @font-face { font-family: 'Hahmlet'; src: url('/fonts/Hahmlet-Regular.ttf') format('truetype'); font-weight: 400; font-style: normal; font-display: swap; } @font-face { font-family: 'Hahmlet'; src: url('/fonts/Hahmlet-Medium.ttf') format('truetype'); font-weight: 500; font-style: normal; font-display: swap; } @@ -23,15 +51,13 @@ :root { color: #fff; background-color: var(--color-primary); + line-height: 1.5; + font-weight: 500; + --default-font-family:'NanumSquareNeo', ui-sans-serif, system-ui, sans-serif; } html { scroll-behavior: auto; } -body { - font-family: 'NanumSquareNeo Var', system-ui, sans-serif; - line-height: 1.5; - font-weight: 500; -} /* 3) font-serif 클래스 사용 시 Hahmlet 적용 */ .font-serif {