Skip to content

Commit c803ff8

Browse files
committed
fix: safe area 테스트
1 parent 144b8ea commit c803ff8

File tree

4 files changed

+230
-52
lines changed

4 files changed

+230
-52
lines changed

apps/tuk-web/src/app/invite/gathering/[gatheringId]/src/components/InviteGathering.tsx

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,98 @@
1+
// 'use client';
2+
3+
// import { QueryErrorResetBoundary } from '@tanstack/react-query';
4+
// import { Suspense } from 'react';
5+
// import { ErrorBoundary } from 'react-error-boundary';
6+
7+
// import InviteGatheringContent from '@/app/invite/gathering/[gatheringId]/src/components/InviteGatheringContent';
8+
// import InviteGatheringErrorFallback from '@/app/invite/gathering/[gatheringId]/src/components/InviteGatheringErrorFallback';
9+
// import InviteGatheringSkeleton from '@/app/invite/gathering/[gatheringId]/src/components/InviteGatheringSkeleton';
10+
// import SkeletonGuard from '@/app/invite/meet/[meetId]/src/components/SkeletonGuard';
11+
// import { BackgroundTemplate, Button } from '@/shared/components';
12+
// import { useParam } from '@/shared/hooks/useParam';
13+
14+
// const InviteGathering = () => {
15+
// const gatheringId = Number(useParam('gatheringId'));
16+
17+
// return (
18+
// <BackgroundTemplate>
19+
// <BackgroundTemplate.Main className="overflow-y-auto px-5">
20+
// <BackgroundTemplate.Gradient />
21+
22+
// <QueryErrorResetBoundary>
23+
// {({ reset }) => (
24+
// <ErrorBoundary onReset={reset} FallbackComponent={InviteGatheringErrorFallback}>
25+
// <SkeletonGuard minMs={250} skeleton={<InviteGatheringSkeleton />}>
26+
// <Suspense fallback={null}>
27+
// <InviteGatheringContent />
28+
// </Suspense>
29+
// </SkeletonGuard>
30+
// </ErrorBoundary>
31+
// )}
32+
// </QueryErrorResetBoundary>
33+
34+
// <BackgroundTemplate.CTA>
35+
// <Button
36+
// className="w-full"
37+
// onClick={() =>
38+
// (window.location.href = `tuk-app://tuk/join-gathering?gatheringId=${gatheringId}`)
39+
// }
40+
// >
41+
// 입장하기
42+
// </Button>
43+
// </BackgroundTemplate.CTA>
44+
// </BackgroundTemplate.Main>
45+
// </BackgroundTemplate>
46+
// );
47+
// };
48+
49+
// export default InviteGathering;
50+
151
'use client';
252

353
import { QueryErrorResetBoundary } from '@tanstack/react-query';
454
import { Suspense } from 'react';
555
import { ErrorBoundary } from 'react-error-boundary';
656

7-
import InviteGatheringContent from '@/app/invite/gathering/[gatheringId]/src/components/InviteGatheringContent';
8-
import InviteGatheringErrorFallback from '@/app/invite/gathering/[gatheringId]/src/components/InviteGatheringErrorFallback';
9-
import InviteGatheringSkeleton from '@/app/invite/gathering/[gatheringId]/src/components/InviteGatheringSkeleton';
57+
import InviteGatheringContent from './InviteGatheringContent';
58+
import InviteGatheringErrorFallback from './InviteGatheringErrorFallback';
59+
import InviteGatheringSkeleton from './InviteGatheringSkeleton';
60+
1061
import SkeletonGuard from '@/app/invite/meet/[meetId]/src/components/SkeletonGuard';
1162
import { BackgroundTemplate, Button } from '@/shared/components';
1263
import { useParam } from '@/shared/hooks/useParam';
64+
import { cn } from '@/shared/lib';
1365

1466
const InviteGathering = () => {
1567
const gatheringId = Number(useParam('gatheringId'));
1668

1769
return (
1870
<BackgroundTemplate>
71+
{/* Main의 overflow는 템플릿 스타일을 유지하면서,
72+
내부 컨테이너로 스크롤/여백 제어 */}
1973
<BackgroundTemplate.Main className="overflow-y-auto px-5">
2074
<BackgroundTemplate.Gradient />
21-
22-
<QueryErrorResetBoundary>
23-
{({ reset }) => (
24-
<ErrorBoundary onReset={reset} FallbackComponent={InviteGatheringErrorFallback}>
25-
<SkeletonGuard minMs={250} skeleton={<InviteGatheringSkeleton />}>
26-
<Suspense fallback={null}>
27-
<InviteGatheringContent />
28-
</Suspense>
29-
</SkeletonGuard>
30-
</ErrorBoundary>
75+
{/* 스크롤 영역 + 하단 CTA 안전 여백 확보 */}+{' '}
76+
<div
77+
className={cn(
78+
'relative mx-auto w-full max-w-[600px]',
79+
'min-h-[100dvh]', // 사파리 주소창에 둔감
80+
'pb-[calc(120px+env(safe-area-inset-bottom,0px))]' // env 기본값 명시
3181
)}
32-
</QueryErrorResetBoundary>
33-
82+
>
83+
<QueryErrorResetBoundary>
84+
{({ reset }) => (
85+
<ErrorBoundary onReset={reset} FallbackComponent={InviteGatheringErrorFallback}>
86+
<SkeletonGuard minMs={250} skeleton={<InviteGatheringSkeleton />}>
87+
<Suspense fallback={null}>
88+
<InviteGatheringContent />
89+
</Suspense>
90+
</SkeletonGuard>
91+
</ErrorBoundary>
92+
)}
93+
</QueryErrorResetBoundary>
94+
</div>
95+
{/* 하단 CTA (고정) */}
3496
<BackgroundTemplate.CTA>
3597
<Button
3698
className="w-full"

apps/tuk-web/src/app/invite/gathering/[gatheringId]/src/components/InviteGatheringContent.tsx

Lines changed: 116 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,100 @@
1+
// import { useSuspenseQuery } from '@tanstack/react-query';
2+
// import { useEffect, useState } from 'react';
3+
4+
// import { gatheringAPIService } from '@/app/invite/gathering/[gatheringId]/src/service';
5+
// import { CardFrame } from '@/app/proposal/[proposalId]/detail/components/GatheringProposalContent';
6+
// import AppInstallBanner from '@/shared/components/AppInstallBanner';
7+
// import { useParam } from '@/shared/hooks/useParam';
8+
// import { cn } from '@/shared/lib';
9+
10+
// const BANNER_KEY = 'gathering-banner-dismissed-at';
11+
// const BANNER_RESHOW_MINUTES = 30;
12+
13+
// const InviteGatheringContent = () => {
14+
// const gatheringId = Number(useParam('gatheringId'));
15+
16+
// const { data: proposalDetail } = useSuspenseQuery({
17+
// queryKey: ['getGatheringName', gatheringId],
18+
// queryFn: () => gatheringAPIService.getGatheringName(gatheringId),
19+
// });
20+
21+
// const [showBanner, setShowBanner] = useState(false);
22+
23+
// const handleCloseBanner = () => {
24+
// localStorage.setItem(BANNER_KEY, Date.now().toString());
25+
// setShowBanner(false);
26+
// };
27+
28+
// useEffect(() => {
29+
// const dismissedAt = localStorage.getItem(BANNER_KEY);
30+
// const now = Date.now();
31+
32+
// if (!dismissedAt) {
33+
// setShowBanner(true);
34+
// } else {
35+
// const dismissedTime = parseInt(dismissedAt, 10);
36+
// const thirtyMinutes = BANNER_RESHOW_MINUTES * 60 * 1000;
37+
// if (now - dismissedTime > thirtyMinutes) {
38+
// setShowBanner(true);
39+
// }
40+
// }
41+
// }, []);
42+
43+
// return (
44+
// <>
45+
// {showBanner && <AppInstallBanner onClose={handleCloseBanner} />}
46+
47+
// <h2
48+
// className={cn(
49+
// 'serif-title-22-M font-bold text-gray-900',
50+
// showBanner ? 'mt-[6.875rem]' : 'mt-[1.875rem]'
51+
// )}
52+
// >
53+
// 모임에
54+
// <br />
55+
// 참여하시겠어요?
56+
// </h2>
57+
58+
// <div className="relative mt-[70px] flex flex-col items-center justify-center">
59+
// <div className="h-[320px] w-[278px] rounded-[10px] bg-gray-50" />
60+
// <div
61+
// className={cn(
62+
// 'absolute bottom-[-80px] left-1/2 h-[421px] w-[408px] -translate-x-1/2 translate-y-0'
63+
// )}
64+
// >
65+
// <div className="relative size-full">
66+
// <div className="absolute inset-0 z-0">
67+
// <CardFrame />
68+
// </div>
69+
70+
// {proposalDetail.data.gatheringName && (
71+
// <div className="serif-body-16-M absolute left-1/2 top-[180px] z-[1] -translate-x-1/2 text-center text-gray-900">
72+
// {proposalDetail.data.gatheringName}
73+
// </div>
74+
// )}
75+
// </div>
76+
// </div>
77+
// </div>
78+
// </>
79+
// );
80+
// };
81+
82+
// export default InviteGatheringContent;
83+
84+
'use client';
85+
186
import { useSuspenseQuery } from '@tanstack/react-query';
2-
import { useEffect, useState } from 'react';
87+
import { useState } from 'react';
88+
89+
import { gatheringAPIService } from '../service';
390

4-
import { gatheringAPIService } from '@/app/invite/gathering/[gatheringId]/src/service';
591
import { CardFrame } from '@/app/proposal/[proposalId]/detail/components/GatheringProposalContent';
692
import AppInstallBanner from '@/shared/components/AppInstallBanner';
793
import { useParam } from '@/shared/hooks/useParam';
894
import { cn } from '@/shared/lib';
995

10-
const BANNER_KEY = 'gathering-banner-dismissed-at';
11-
const BANNER_RESHOW_MINUTES = 30;
96+
// const BANNER_KEY = 'gathering-banner-dismissed-at';
97+
// const BANNER_RESHOW_MINUTES = 30;
1298

1399
const InviteGatheringContent = () => {
14100
const gatheringId = Number(useParam('gatheringId'));
@@ -18,50 +104,50 @@ const InviteGatheringContent = () => {
18104
queryFn: () => gatheringAPIService.getGatheringName(gatheringId),
19105
});
20106

21-
const [showBanner, setShowBanner] = useState(false);
107+
const [showBanner, setShowBanner] = useState(true);
22108

23109
const handleCloseBanner = () => {
24-
localStorage.setItem(BANNER_KEY, Date.now().toString());
110+
// localStorage.setItem(BANNER_KEY, Date.now().toString());
25111
setShowBanner(false);
26112
};
27113

28-
useEffect(() => {
29-
const dismissedAt = localStorage.getItem(BANNER_KEY);
30-
const now = Date.now();
31-
32-
if (!dismissedAt) {
33-
setShowBanner(true);
34-
} else {
35-
const dismissedTime = parseInt(dismissedAt, 10);
36-
const thirtyMinutes = BANNER_RESHOW_MINUTES * 60 * 1000;
37-
if (now - dismissedTime > thirtyMinutes) {
38-
setShowBanner(true);
39-
}
40-
}
41-
}, []);
114+
// useEffect(() => {
115+
// const dismissedAt = localStorage.getItem(BANNER_KEY);
116+
// const now = Date.now();
117+
// if (!dismissedAt) {
118+
// setShowBanner(true);
119+
// } else {
120+
// const dismissedTime = parseInt(dismissedAt, 10);
121+
// const thirtyMinutes = BANNER_RESHOW_MINUTES * 60 * 1000;
122+
// if (now - dismissedTime > thirtyMinutes) {
123+
// setShowBanner(true);
124+
// }
125+
// }
126+
// }, []);
42127

43128
return (
44129
<>
45130
{showBanner && <AppInstallBanner onClose={handleCloseBanner} />}
46131

132+
{/* 배너 노출 시: safe-area 포함하여 충분한 상단 여백 확보 */}
47133
<h2
48134
className={cn(
49135
'serif-title-22-M font-bold text-gray-900',
50-
showBanner ? 'mt-[6.875rem]' : 'mt-[1.875rem]'
136+
showBanner ? 'mt-[calc(110px+env(safe-area-inset-top,0px))]' : 'mt-[1.875rem]'
51137
)}
52138
>
53139
모임에
54140
<br />
55141
참여하시겠어요?
56142
</h2>
57143

58-
<div className="relative mt-[70px] flex flex-col items-center justify-center">
144+
{/* 카드/프레임 영역 */}
145+
<div className="relative mt-[56px] flex flex-col items-center justify-center">
146+
{/* 카드 배경 박스 (그림자/여백 안정화) */}
59147
<div className="h-[320px] w-[278px] rounded-[10px] bg-gray-50" />
60-
<div
61-
className={cn(
62-
'absolute bottom-[-80px] left-1/2 h-[421px] w-[408px] -translate-x-1/2 translate-y-0'
63-
)}
64-
>
148+
149+
{/* 프레임을 살짝 겹치게 띄워서 시각적 포커스 */}
150+
<div className="pointer-events-none absolute bottom-[-80px] left-1/2 h-[421px] w-[408px] -translate-x-1/2">
65151
<div className="relative size-full">
66152
<div className="absolute inset-0 z-0">
67153
<CardFrame />
@@ -75,6 +161,9 @@ const InviteGatheringContent = () => {
75161
</div>
76162
</div>
77163
</div>
164+
165+
{/* 본문 끝에서 한 번 더 하단 여백 확보 (작은 기기 대비) */}
166+
<div className="h-8" />
78167
</>
79168
);
80169
};

apps/tuk-web/src/app/invite/gathering/[gatheringId]/src/components/SplashGate.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@
33
import Image from 'next/image';
44
import { useEffect, useState } from 'react';
55

6+
import { cn } from '@/shared/lib';
7+
68
type SplashGateProps = {
7-
/** 최소 표시 시간 (ms) */
89
minMs?: number;
9-
/** 로고 경로 (public/ 기준) */
1010
logoSrc?: string;
11-
/** 로고 크기(px) */
1211
logoSize?: number;
13-
/** 페이드아웃 시간(ms) */
1412
fadeMs?: number;
1513
children: React.ReactNode;
1614
};
@@ -26,9 +24,7 @@ export default function SplashGate({
2624
const [fadeOut, setFadeOut] = useState(false);
2725

2826
useEffect(() => {
29-
// 최소 표시 시간 보장
3027
const t = setTimeout(() => {
31-
// 모션 감소 환경이면 바로 제거
3228
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
3329
if (media.matches || fadeMs === 0) {
3430
setShow(false);
@@ -50,8 +46,10 @@ export default function SplashGate({
5046
<div
5147
aria-busy="true"
5248
aria-live="polite"
53-
className={`fixed inset-0 z-[9999] flex items-center justify-center bg-white-default ${fadeOut ? 'opacity-0 transition-opacity' : 'opacity-100'} `}
54-
// Tailwind transition-duration 커스텀
49+
className={cn(
50+
'fixed inset-0 z-[9999] flex items-center justify-center bg-white-default',
51+
fadeOut ? 'opacity-0 transition-opacity' : 'opacity-100'
52+
)}
5553
style={{ transitionDuration: `${fadeMs}ms` }}
5654
>
5755
<Image src={logoSrc} alt="App Logo" width={logoSize} height={logoSize} priority />

apps/tuk-web/src/shared/components/AppInstallBanner.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,39 @@
1+
// import { CloseIcon24 } from '@/shared/components/icon';
2+
3+
// const AppInstallBanner = ({ onClose }: { onClose: () => void }) => {
4+
// return (
5+
// <div className="fixed left-1/2 top-0 z-20 flex h-14 w-full max-w-[600px] -translate-x-1/2 items-center justify-between bg-gray-50 px-5 text-black-default">
6+
// <div className="flex items-center gap-2">
7+
// <button onClick={onClose}>
8+
// <CloseIcon24 />
9+
// </button>
10+
// <div className="flex items-center gap-2">
11+
// <TukLogo />
12+
// <p className="pretendard-body-12-R text-gray-900">앱으로 편하게 만남을 이어나가보세요</p>
13+
// </div>
14+
// </div>
15+
16+
// <button
17+
// className="pretendard-body-12-B rounded-[1.25rem] bg-gray-900 px-2.5 py-2 text-white-default"
18+
// onClick={() => (window.location.href = 'tuk-app://tuk')}
19+
// >
20+
// 앱 열기
21+
// </button>
22+
// </div>
23+
// );
24+
// };
25+
26+
// export default AppInstallBanner;
27+
28+
'use client';
29+
130
import { CloseIcon24 } from '@/shared/components/icon';
231

332
const AppInstallBanner = ({ onClose }: { onClose: () => void }) => {
433
return (
5-
<div className="fixed left-1/2 top-0 z-20 flex h-14 w-full max-w-[600px] -translate-x-1/2 items-center justify-between bg-gray-50 px-5 text-black-default">
34+
<div className="fixed left-1/2 top-0 z-20 flex h-[calc(56px+env(safe-area-inset-top))] w-full max-w-[600px] -translate-x-1/2 items-center justify-between bg-gray-50 px-5 pt-[max(0px,env(safe-area-inset-top))] text-black-default shadow-[0_1px_0_0_rgba(0,0,0,0.04)] backdrop-blur-[2px]">
635
<div className="flex items-center gap-2">
7-
<button onClick={onClose}>
36+
<button onClick={onClose} aria-label="배너 닫기">
837
<CloseIcon24 />
938
</button>
1039
<div className="flex items-center gap-2">

0 commit comments

Comments
 (0)