Skip to content

Commit d53742f

Browse files
authored
Merge pull request #370 from mosu-dev/feature#368
Feature#368 모의수능 할인 배너 추가
2 parents 0c8a875 + 7114b36 commit d53742f

8 files changed

Lines changed: 135 additions & 29 deletions

File tree

mosu-app/src/apps/site.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import snu from "@/widgets/about/assets/서울대.png";
77
import medical from "@/widgets/about/assets/의대.png";
88

99
export const siteConfiguration = {
10-
globalLayoutExceptions: ["/", "/auth/kmc", "/auth/kakao/redirect", "/payments/redirect"],
10+
globalLayoutExceptions: ["/auth/kmc", "/auth/kakao/redirect", "/payments/redirect"],
1111

1212
navTop: [
1313
{

mosu-app/src/apps/ui/MobileNavTop.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { siteConfiguration } from "@/apps/site";
77

88
import { useAuth } from "@/features/auth/hooks/useAuth";
99
import { useSignOut } from "@/features/auth/services/signOut";
10+
import { useBannerVisibility } from "@/features/banner/contexts/BannerProvider";
11+
import { BANNER_HEIGHT } from "@/features/banner/ui/Banner";
1012

1113
import { useIsMounted } from "@/shared/hooks/useIsMounted";
1214
import { useToggle } from "@/shared/hooks/useToggle";
@@ -118,14 +120,18 @@ interface MobileNavContainerProps {
118120
}
119121

120122
const MobileNavContainer = ({ isOpen, children }: MobileNavContainerProps) => {
123+
const { isVisible } = useBannerVisibility();
124+
121125
return (
122-
<div
123-
className={`fixed top-[60px] right-0 left-0 z-[95] w-full transform overflow-hidden bg-white shadow-lg transition-all duration-700 ease-out ${
124-
isOpen ? "max-h-100 opacity-100" : "max-h-0 opacity-0"
125-
}`}
126+
<nav
127+
className={cn(
128+
"fixed right-0 left-0 z-[95] w-full transform overflow-hidden bg-white shadow-lg transition-all duration-700 ease-out",
129+
isOpen ? "max-h-100 opacity-100" : "max-h-0 opacity-0",
130+
)}
131+
style={{ top: isVisible ? BANNER_HEIGHT + 60 : 60 }}
126132
>
127133
<ul className="divide-y divide-gray-200 text-center">{children}</ul>
128-
</div>
134+
</nav>
129135
);
130136
};
131137

16.1 KB
Loading
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import dynamic from "next/dynamic";
2+
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from "react";
3+
4+
const Banner = dynamic(() => import("@/features/banner/ui/Banner").then((mod) => ({ default: mod.Banner })), {
5+
ssr: false,
6+
});
7+
8+
export interface BannerVisibility {
9+
isVisible: boolean;
10+
showBanner: () => void;
11+
hideBanner: () => void;
12+
}
13+
14+
export const BannerContext = createContext<BannerVisibility | null>(null);
15+
16+
export const BannerProvider = ({ children }: PropsWithChildren) => {
17+
const [isVisible, setIsVisible] = useState(true);
18+
19+
const showBanner = useCallback(() => setIsVisible(true), []);
20+
const hideBanner = useCallback(() => setIsVisible(false), []);
21+
22+
return (
23+
<BannerContext.Provider
24+
value={useMemo(
25+
() => ({
26+
isVisible,
27+
showBanner,
28+
hideBanner,
29+
}),
30+
[hideBanner, isVisible, showBanner],
31+
)}
32+
>
33+
{isVisible && <Banner />}
34+
{children}
35+
</BannerContext.Provider>
36+
);
37+
};
38+
39+
export const useBannerVisibility = () => {
40+
const context = useContext(BannerContext);
41+
if (!context) {
42+
throw new Error("useBannerVisibility 는 BannerVisibilityProvider 내부에서만 사용 가능합니다.");
43+
}
44+
return context;
45+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { X } from "lucide-react";
2+
import Image from "next/image";
3+
import { createPortal } from "react-dom";
4+
5+
import imgDiscount from "@/features/banner/assets/img-discount.png";
6+
import { useBannerVisibility } from "@/features/banner/contexts/BannerProvider";
7+
8+
export const BANNER_PORTAL_ID = "banner-container";
9+
export const BANNER_HEIGHT = 70;
10+
11+
export const Banner = () => {
12+
const { isVisible, hideBanner } = useBannerVisibility();
13+
14+
return (
15+
isVisible &&
16+
createPortal(
17+
<section className="w-full bg-black" style={{ height: BANNER_HEIGHT }}>
18+
<div
19+
className="relative flex w-full justify-start px-4 md:justify-center"
20+
style={{
21+
height: BANNER_HEIGHT,
22+
background: "linear-gradient(to bottom, #1d1d1d28 0%, #ff1d3828 100%)",
23+
}}
24+
>
25+
<div className="flex items-center justify-center">
26+
<article>
27+
<h1 className="text-base font-bold text-white md:text-lg">
28+
얼리버드 특가 9월중으로 <span className="text-red-500">곧 종료</span>됩니다.
29+
</h1>
30+
<p className="text-xs text-white md:text-sm">8월 27일 기준 모수 이용자 1000명 돌파!</p>
31+
</article>
32+
<Image
33+
src={imgDiscount}
34+
className="ml-2 block"
35+
width={70}
36+
height={70}
37+
alt="얼리버드 특가 진행중"
38+
/>
39+
</div>
40+
41+
<button
42+
className="absolute right-2 block h-full hover:cursor-pointer md:right-5"
43+
aria-label="배너 닫기"
44+
onClick={hideBanner}
45+
>
46+
<X color="#fff" />
47+
</button>
48+
</div>
49+
</section>,
50+
document.getElementById(BANNER_PORTAL_ID) as HTMLElement,
51+
)
52+
);
53+
};

mosu-app/src/pages/_app.tsx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { siteConfiguration } from "@/apps/site";
1515
import "@/apps/styles/globals.css";
1616
import { GlobalLayout } from "@/apps/ui/GlobalLayout";
1717

18+
import { BannerProvider } from "@/features/banner/contexts/BannerProvider";
19+
1820
type AppPropsWithLayout = AppProps & {
1921
Component: NextPage & { layout?: (page: React.ReactNode) => React.ReactNode };
2022
};
@@ -33,25 +35,27 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
3335
return (
3436
<CookiesProvider>
3537
<QueryClientProvider client={queryClient}>
36-
{process.env.NEXT_PUBLIC_GTM_ID && <GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID} />}
37-
{process.env.NEXT_PUBLIC_GA_ID && <GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />}
38-
<ToastContainer
39-
position="top-center"
40-
autoClose={1500}
41-
hideProgressBar={false}
42-
newestOnTop={false}
43-
closeOnClick={false}
44-
rtl={false}
45-
pauseOnFocusLoss
46-
draggable
47-
pauseOnHover
48-
theme="light"
49-
/>
50-
{siteConfiguration.globalLayoutExceptions.includes(router.pathname) ? (
51-
Page
52-
) : (
53-
<GlobalLayout>{Page}</GlobalLayout>
54-
)}
38+
<BannerProvider>
39+
{process.env.NEXT_PUBLIC_GTM_ID && <GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID} />}
40+
{process.env.NEXT_PUBLIC_GA_ID && <GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />}
41+
<ToastContainer
42+
position="top-center"
43+
autoClose={1500}
44+
hideProgressBar={false}
45+
newestOnTop={false}
46+
closeOnClick={false}
47+
rtl={false}
48+
pauseOnFocusLoss
49+
draggable
50+
pauseOnHover
51+
theme="light"
52+
/>
53+
{siteConfiguration.globalLayoutExceptions.includes(router.pathname) ? (
54+
Page
55+
) : (
56+
<GlobalLayout>{Page}</GlobalLayout>
57+
)}
58+
</BannerProvider>
5559
</QueryClientProvider>
5660
</CookiesProvider>
5761
);

mosu-app/src/pages/_document.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ export default function Document() {
77
<meta property="og:image" content="/preview.png" />
88
<meta name="twitter:card" content="/preview.png" />
99
</Head>
10+
1011
<body className="antialiased">
12+
<aside id="banner-container" className="relative z-[999] w-full"></aside>
1113
<Main />
1214
<NextScript />
1315
</body>

mosu-app/src/pages/index.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import gsap from "gsap";
22
import { ScrollTrigger } from "gsap/ScrollTrigger";
33

4-
import { Footer } from "@/apps/ui/Footer";
5-
import { NavTop } from "@/apps/ui/NavTop";
64
import { SiteMetadata } from "@/apps/ui/SiteMetadata";
75

86
import { CoworkerSectionUnderMobile } from "@/widgets/home/CoworkerSectionUnderMobile";
@@ -25,7 +23,6 @@ export default function Home() {
2523
title="모의가 아닌 진짜 수능, 모수"
2624
content="실제 학교 교실에서 수능과 동일한 시간에, 수험생이 직접 선택한 모의고사를 가져와 응시하는 실전형 수능 시뮬레이션 프로그램입니다."
2725
/>
28-
<NavTop />
2926
<HeroSection />
3027
<IntroSection />
3128
<ProblemSection />
@@ -46,7 +43,6 @@ export default function Home() {
4643
<ReviewSection />
4744
<FooterSection />
4845
<PartnershipSection />
49-
<Footer />
5046
</main>
5147
);
5248
}

0 commit comments

Comments
 (0)