Skip to content

Commit f071906

Browse files
authored
Style/main page 2#122 (#132)
* [style]마이페이지 슬라이드 퍼블리싱 * [style] 메인페이지 슬라이드 * [docs]provider문서 정리 * [style] 메인페이지 스타일 간격조정 * [feat]알림 설정처리 * [stype]메인페이지 반응형 * [feat] 나만의 바 삭제 동기화 * [feat] 마이페이지 전체삭제 * [style] 메인페이지 반응형 * [feat] 마이페이지 푸시 오류 수정 * [docs]미 사용 파일 삭제 * [style] 메인3d이미지 스크롤 실험div제거
1 parent 61bd59c commit f071906

39 files changed

+896
-80
lines changed

src/app/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import KaKaoScript from './api/kakao/KaKaoScript';
88
import 'swiper/css';
99
import 'swiper/css/navigation';
1010
import 'swiper/css/pagination';
11-
import Provider from '@/shared/api/Provider';
11+
1212
import ClientInitHook from '@/domains/login/components/ClientInitHook';
13+
import Provider from '@/shared/provider/Provider';
1314

1415
export const metadata: Metadata = {
1516
title: { default: 'SSOUL', template: 'SSOUL | %s' },

src/app/page.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import Landing from '@/domains/shared/components/3d/Landing';
22

3+
import MainSlide from '@/domains/main/components/mainSlide/components/MainSlide';
4+
35
export default function Home() {
4-
return <Landing />;
6+
return (
7+
<div className="page-layout max-w-full">
8+
<Landing />
9+
<MainSlide />
10+
</div>
11+
);
512
}

src/app/recipe/[id]/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getApi } from '@/app/api/config/appConfig';
2-
import DetailMain from '@/domains/recipe/details/DetailMain';
2+
import DetailMain from '@/domains/recipe/components/details/DetailMain';
3+
34
import StarBg from '@/domains/shared/components/star-bg/StarBg';
45
import { Metadata } from 'next';
56

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
'use client';
2+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
3+
import MainSlideAbv from './MainSlideAbv';
4+
5+
import gsap from 'gsap';
6+
import { ScrollTrigger } from 'gsap/ScrollTrigger';
7+
import MobileSlide from './mobile/MobileSlide';
8+
import MainSlideIntro from './MainSlideIntro';
9+
import MainSlideTest from './MainSlideTest';
10+
import MainSlideCommunity from './MainSlideCommunity';
11+
12+
gsap.registerPlugin(ScrollTrigger);
13+
14+
function MainSlide() {
15+
const root = useRef<HTMLDivElement>(null);
16+
const [isMobile, setIsMobile] = useState(false);
17+
const [mounted, setMounted] = useState(false);
18+
const cleanupFnRef = useRef<(() => void) | null>(null);
19+
const resizeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
20+
21+
// 초기 마운트
22+
useEffect(() => {
23+
setIsMobile(window.innerWidth < 1024);
24+
setMounted(true);
25+
26+
const handleResize = () => {
27+
// 디바운스: resize 이벤트를 200ms 지연
28+
if (resizeTimeoutRef.current) {
29+
clearTimeout(resizeTimeoutRef.current);
30+
}
31+
32+
resizeTimeoutRef.current = setTimeout(() => {
33+
const newIsMobile = window.innerWidth < 1024;
34+
35+
// 모바일 ↔ 데스크탑 전환 시에만 cleanup 실행
36+
if (newIsMobile !== isMobile) {
37+
// GSAP을 먼저 완전히 정리
38+
if (cleanupFnRef.current) {
39+
cleanupFnRef.current();
40+
cleanupFnRef.current = null;
41+
}
42+
43+
// 상태 업데이트
44+
setIsMobile(newIsMobile);
45+
}
46+
}, 200);
47+
};
48+
49+
window.addEventListener('resize', handleResize);
50+
return () => {
51+
window.removeEventListener('resize', handleResize);
52+
if (resizeTimeoutRef.current) {
53+
clearTimeout(resizeTimeoutRef.current);
54+
}
55+
if (cleanupFnRef.current) {
56+
cleanupFnRef.current();
57+
}
58+
};
59+
}, [isMobile]);
60+
61+
// GSAP 초기화 - 데스크탑에서만
62+
useLayoutEffect(() => {
63+
if (!mounted) return;
64+
if (isMobile) return;
65+
if (!root.current) return;
66+
67+
const el = root.current;
68+
const stage = el.querySelector('.stage') as HTMLElement;
69+
if (!stage) return;
70+
71+
// 약간의 지연을 줘서 DOM이 안정화되도록
72+
const timer = setTimeout(() => {
73+
if (!root.current) return;
74+
75+
const ctx = gsap.context(() => {
76+
const panels = Array.from(el.querySelectorAll<HTMLElement>('.panel'));
77+
const tl = gsap.timeline({ paused: true, defaults: { ease: 'none' } });
78+
79+
panels.forEach((panel, i) => {
80+
const c = panel.querySelector<HTMLElement>('.slide-content');
81+
if (!c) return;
82+
const stageW = () => stage.clientWidth;
83+
const contentW = () => c.getBoundingClientRect().width;
84+
85+
gsap.set(c, { x: stageW() });
86+
87+
tl.to(
88+
c,
89+
{
90+
x: () => stageW() - contentW(),
91+
duration: 1,
92+
immediateRender: false,
93+
onStart: () => c.classList.remove('invisible'),
94+
},
95+
i
96+
);
97+
});
98+
99+
ScrollTrigger.create({
100+
trigger: el,
101+
start: 'top top',
102+
end: `+=${panels.length * 100}%`,
103+
pin: true,
104+
scrub: true,
105+
animation: tl,
106+
invalidateOnRefresh: true,
107+
});
108+
109+
ScrollTrigger.refresh();
110+
}, root);
111+
112+
// cleanup 함수를 ref에 저장
113+
cleanupFnRef.current = () => {
114+
// ScrollTrigger를 먼저 완전히 제거
115+
const allTriggers = ScrollTrigger.getAll();
116+
allTriggers.forEach((st) => {
117+
if (st.trigger === el || el.contains(st.trigger as Node)) {
118+
st.kill(true);
119+
}
120+
});
121+
122+
// GSAP context revert
123+
try {
124+
ctx.revert();
125+
} catch (e) {
126+
// 무시
127+
}
128+
129+
// 혹시 남아있는 pin-spacer 수동 제거
130+
const pinSpacers = document.querySelectorAll('.pin-spacer');
131+
pinSpacers.forEach((spacer) => {
132+
if (spacer.contains(el) || el.contains(spacer)) {
133+
try {
134+
const child = spacer.querySelector('section');
135+
if (child && spacer.parentElement) {
136+
spacer.parentElement.appendChild(child);
137+
}
138+
spacer.remove();
139+
} catch (e) {
140+
// 무시
141+
}
142+
}
143+
});
144+
};
145+
}, 50);
146+
147+
return () => {
148+
clearTimeout(timer);
149+
if (cleanupFnRef.current) {
150+
cleanupFnRef.current();
151+
cleanupFnRef.current = null;
152+
}
153+
};
154+
}, [isMobile, mounted]);
155+
156+
// SSR 방지
157+
if (!mounted) {
158+
return null;
159+
}
160+
161+
return (
162+
<>
163+
{isMobile ? (
164+
<MobileSlide key="mobile" />
165+
) : (
166+
<section key="desktop" ref={root} className="h-screen">
167+
<div className="stage relative w-full h-full overflow-hidden">
168+
<div className="panel absolute inset-0">
169+
<MainSlideIntro />
170+
</div>
171+
<div className="panel absolute inset-0">
172+
<MainSlideTest />
173+
</div>
174+
<div className="panel absolute inset-0">
175+
<MainSlideCommunity />
176+
</div>
177+
<div className="panel absolute inset-0">
178+
<MainSlideAbv />
179+
</div>
180+
</div>
181+
</section>
182+
)}
183+
</>
184+
);
185+
}
186+
187+
export default MainSlide;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Ssury1 from '@/shared/assets/ssury/ssury_level1.webp';
2+
import Ssury2 from '@/shared/assets/ssury/ssury_level2.webp';
3+
import Ssury3 from '@/shared/assets/ssury/ssury_level3.webp';
4+
import Ssury4 from '@/shared/assets/ssury/ssury_level4.webp';
5+
import Ssury5 from '@/shared/assets/ssury/ssury_level5.webp';
6+
import Ssury6 from '@/shared/assets/ssury/ssury_level6.webp';
7+
import MainSsuryDrunk from './MainSsuryDrunk';
8+
9+
function MainSlideAbv() {
10+
const SSURY_DRUNK = [
11+
{
12+
id: 1,
13+
src: Ssury1,
14+
abv: 5,
15+
},
16+
{
17+
id: 2,
18+
src: Ssury2,
19+
abv: 11,
20+
},
21+
{
22+
id: 3,
23+
src: Ssury3,
24+
abv: 26,
25+
},
26+
{
27+
id: 4,
28+
src: Ssury4,
29+
abv: 46,
30+
},
31+
{
32+
id: 5,
33+
src: Ssury5,
34+
abv: 66,
35+
},
36+
{
37+
id: 6,
38+
src: Ssury6,
39+
abv: 86,
40+
},
41+
];
42+
43+
return (
44+
<div className="slide-content w-1/2 h-full bg-[#84739e] rounded-tl-[30px] rounded-bl-[30px] p-15 flex flex-col justify-center">
45+
<div className="flex flex-col gap-15">
46+
<span className="font-black text-[32px]">3</span>
47+
<div className="flex flex-col gap-5">
48+
<h2 className="text-5xl font-black text-shadow-[0_4px_6px_rgb(255_255_255_/0.25)]">
49+
내 알콜도수 UP
50+
</h2>
51+
<p className="text-xl xl:text-2xl font-normal leading-[1.5]">
52+
5도 부터 시작하는 내 알콜도수 <br />글 작성,댓글,좋아요 / 킵으로 알콜도수 UP! <br />
53+
알콜도수에 따라 변하는 쑤리(SSURY)를 보는 재미도 있어요.
54+
</p>
55+
</div>
56+
<div className=" h-[190px] flex flex-col gap-2">
57+
<ul className="flex gap-[5%] xl:gap-[10%]">
58+
{SSURY_DRUNK.map(({ id, src, abv }) => (
59+
<li key={id}>
60+
<MainSsuryDrunk src={src} abv={abv} />
61+
</li>
62+
))}
63+
</ul>
64+
<div className="w-full h-3 border border-gray rounded-full relative">
65+
<span className="absolute top-0 left-0 h-full rounded-full bg-gradient-to-r from-[#FFCA8D] to-[#FA2424] w-1/2"></span>
66+
</div>
67+
</div>
68+
</div>
69+
</div>
70+
);
71+
}
72+
export default MainSlideAbv;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
function MainSlideCommunity() {
2+
return (
3+
<div className="slide-content w-3/5 h-full p-15 bg-[#77688d] rounded-tl-[30px] rounded-bl-[30px] flex flex-col justify-center">
4+
<div className="flex flex-col justify-center">
5+
<div className="flex flex-col gap-15">
6+
<span className="text-[32px] font-black">2</span>
7+
<div className="flex flex-col gap-5">
8+
<h2 className="text-5xl text-secondary font-black text-shadow-[0_4px_6px_rgb(255_255_255_/0.25)]">
9+
술술 즐기는, 커뮤니티
10+
</h2>
11+
<p className="text-xl xl:text-2xl text-secondary font-normal leading-[1.5]">
12+
칵테일에 대해 물어볼 곳이 없어 목이 마른 당신! <br />
13+
초보자부터 애호가까지, Ssoul에서는 누구나 칵테일 이야기를 나눌 수 있어요.
14+
<br />
15+
회원들과 소통하면 내 칵테일 솜씨를 뽐내보세요.
16+
</p>
17+
</div>
18+
</div>
19+
<span className="h-[250px]"></span>
20+
</div>
21+
</div>
22+
);
23+
}
24+
export default MainSlideCommunity;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Keep from '@/shared/assets/icons/keep_36.svg';
2+
import Image from 'next/image';
3+
4+
interface Props {
5+
id: number;
6+
src: string;
7+
cocktailName: string;
8+
}
9+
10+
function MainSlideDummyCard({ src, cocktailName }: Props) {
11+
return (
12+
<div className="relative flex flex-col w-full min-w-[150px] rounded-2xl overflow-hidden bg-white shadow-[0_0_12px_rgba(255,255,255,0.4)]">
13+
<div className="relative w-full h-[100px]">
14+
<Image src={src} fill className="object-cover" alt="" sizes="100px" priority />
15+
</div>
16+
17+
<div className="p-3 flex flex-col gap-1 text-center">
18+
<strong className="text-black text-lg">{cocktailName}</strong>
19+
<span className="text-gray-500 text-sm">+ 상세보기</span>
20+
</div>
21+
<Keep className="absolute top-2 right-2" fill="transparent" />
22+
</div>
23+
);
24+
}
25+
export default MainSlideDummyCard;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import background from '@/shared/assets/images/main_slide.webp';
2+
import Image from 'next/image';
3+
4+
function MainSlideIntro() {
5+
return (
6+
<div className="relative w-full p-12 h-full">
7+
<Image src={background} alt="" fill className="-z-1" />
8+
<div className="flex flex-col gap-8">
9+
<h2 className="text-3xl lg:text-5xl font-bold leading-[1.5] text-secondary text-shadow-[0_4px_6px_rgb(255_255_255_/0.25)]">
10+
칵테일 <br /> 누구나 쉽게 즐길 수 있어요
11+
</h2>
12+
<p className="text-2xl font-normal">
13+
SSOUL의 재밌고 다양한 기능들로 더 친근하게 접해보세요
14+
</p>
15+
</div>
16+
</div>
17+
);
18+
}
19+
export default MainSlideIntro;

0 commit comments

Comments
 (0)