Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions mosu-app/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { SiteMetadata } from "@/apps/ui/SiteMetadata";
import { CoworkerSectionUnderMobile } from "@/widgets/home/CoworkerSectionUnderMobile";
import { FeatureSection } from "@/widgets/home/FeatureSection";
import { FooterSection } from "@/widgets/home/FooterSection";
import { HeroSection } from "@/widgets/home/HeroSection";
// import { HeroSection } from "@/widgets/home/HeroSection";
import { IntroSection } from "@/widgets/home/IntroSection";
import { PartnershipSection } from "@/widgets/home/PartnershipSection";
import { ProblemSection } from "@/widgets/home/ProblemSection";
import { ReviewSection } from "@/widgets/home/ReviewSection";
import { ServiceLocationSection } from "@/widgets/home/ServiceLocationSection";
import { SolutionSection } from "@/widgets/home/SolutionSection";
import { YoutubeHeroSection } from "@/widgets/home/YoutubeHeroSection";

gsap.registerPlugin(ScrollTrigger);

Expand All @@ -23,7 +24,8 @@ export default function Home() {
title="모의가 아닌 진짜 수능, 모수"
content="실제 학교 교실에서 수능과 동일한 시간에, 수험생이 직접 선택한 모의고사를 가져와 응시하는 실전형 수능 시뮬레이션 프로그램입니다."
/>
<HeroSection />
{/* <HeroSection /> */}
<YoutubeHeroSection />
<IntroSection />
<ProblemSection />
<SolutionSection />
Expand Down
17 changes: 17 additions & 0 deletions mosu-app/src/shared/hooks/useFontLoadState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useState } from "react";

export const useFontLoadState = () => {
const [fontLoaded, setFontLoaded] = useState(false);

useEffect(() => {
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => {
setFontLoaded(true);
});
} else {
setTimeout(() => setFontLoaded(true), 100);
}
}, []);

return { fontLoaded };
};
2 changes: 0 additions & 2 deletions mosu-app/src/widgets/home/HeroSection.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@

margin: 0px auto;

@media (max-width: 1023px) {
}
@media (max-width: 768px) {
padding: 0.5rem;
}
Expand Down
160 changes: 160 additions & 0 deletions mosu-app/src/widgets/home/YoutubeHeroSection.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
.yt_hero_section {
&__wrapper {
position: relative;

display: flex;
flex-direction: column;
justify-content: center;

height: calc(100vh - 100px - 70px);
// 100px: header
// 70px: banner

color: #fff;
background-color: #000;

overflow: clip;

@media (max-width: 1023px) {
height: calc(100vh - 60px - 70px);
}
@media (max-width: 768px) {
height: calc(100vh - 60px - 70px);
}
}

&__video {
width: 100%;
margin: 0px auto;
}

&__title {
display: flex;
flex-direction: row;
justify-content: center;

margin: 0px auto;
text-align: center;

overflow: hidden;

font-weight: bold;
font-size: 3.75rem;

@media (max-width: 1023px) {
flex-direction: column;
gap: 0;

font-size: 3rem;
}

@media (max-width: 767px) {
gap: 0;
font-size: 2rem;

line-height: 1.2;
}
}

&__content {
display: flex;
flex-direction: column;
gap: 1rem;

width: 100%;

margin: 0px auto;

@media (max-width: 768px) {
padding: 0.5rem;
}

p {
display: flex;
flex-direction: column;
text-align: center;

width: 100%;

margin-left: auto;
margin-right: auto;

font-size: 1.125rem;

@media (max-width: 767px) {
font-size: 1rem;
}
}

button {
display: flex;
align-items: center;
gap: 0.75rem;

width: fit-content;

margin: 0.5rem auto;
border-radius: 9999px;
padding: 0.5rem 1rem;

background-color: #fff;
transition: background-color 1s;

font-size: 1rem;

@media (max-width: 1023px) {
padding: 0.5rem 1rem;
font-size: 1rem;
}
@media (max-width: 768px) {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}

&:hover {
cursor: pointer;
background-color: #f3f3f3;
}

span:first-child {
color: #000;
}
span:last-child {
aspect-ratio: 1 / 1;
height: fit-content;

padding: 0.25rem;
border-radius: 9999px;

background-color: #000;
}
}
Comment on lines +89 to +131
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Align styles with Link-as-anchor (not button) and improve a11y focus

The CTA is an anchor via Next.js Link; style the anchor instead of a button and add focus-visible.

-        button {
+        a {
             display: flex;
             align-items: center;
             gap: 0.75rem;
 
             width: fit-content;
 
             margin: 0.5rem auto;
             border-radius: 9999px;
             padding: 0.5rem 1rem;
 
             background-color: #fff;
             transition: background-color 1s;
 
             font-size: 1rem;
+            text-decoration: none; /* anchor reset */
 
             @media (max-width: 1023px) {
                 padding: 0.5rem 1rem;
                 font-size: 1rem;
             }
             @media (max-width: 768px) {
                 padding: 0.5rem 1rem;
                 font-size: 0.875rem;
             }
 
             &:hover {
                 cursor: pointer;
                 background-color: #f3f3f3;
             }
+            &:focus-visible {
+                outline: 2px solid #fff;
+                outline-offset: 2px;
+            }
 
             span:first-child {
                 color: #000;
             }
             span:last-child {
                 aspect-ratio: 1 / 1;
                 height: fit-content;
 
                 padding: 0.25rem;
                 border-radius: 9999px;
 
                 background-color: #000;
+                color: #fff; /* ensure chevron is visible on black */
             }
-        }
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
button {
display: flex;
align-items: center;
gap: 0.75rem;
width: fit-content;
margin: 0.5rem auto;
border-radius: 9999px;
padding: 0.5rem 1rem;
background-color: #fff;
transition: background-color 1s;
font-size: 1rem;
@media (max-width: 1023px) {
padding: 0.5rem 1rem;
font-size: 1rem;
}
@media (max-width: 768px) {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
&:hover {
cursor: pointer;
background-color: #f3f3f3;
}
span:first-child {
color: #000;
}
span:last-child {
aspect-ratio: 1 / 1;
height: fit-content;
padding: 0.25rem;
border-radius: 9999px;
background-color: #000;
}
}
a {
display: flex;
align-items: center;
gap: 0.75rem;
width: fit-content;
margin: 0.5rem auto;
border-radius: 9999px;
padding: 0.5rem 1rem;
background-color: #fff;
transition: background-color 1s;
font-size: 1rem;
text-decoration: none; /* anchor reset */
@media (max-width: 1023px) {
padding: 0.5rem 1rem;
font-size: 1rem;
}
@media (max-width: 768px) {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
&:hover {
cursor: pointer;
background-color: #f3f3f3;
}
&:focus-visible {
outline: 2px solid #fff;
outline-offset: 2px;
}
span:first-child {
color: #000;
}
span:last-child {
aspect-ratio: 1 / 1;
height: fit-content;
padding: 0.25rem;
border-radius: 9999px;
background-color: #000;
color: #fff; /* ensure chevron is visible on black */
}
}


footer {
display: flex;
justify-content: center;

width: 100%;
height: fit-content;

margin: 0px auto;

animation: bounceWithOpacity 1.5s infinite;

@keyframes bounceWithOpacity {
0% {
transform: translateY(0);
opacity: 1;
}
50% {
transform: translateY(-10px);
opacity: 0.5;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
}
56 changes: 56 additions & 0 deletions mosu-app/src/widgets/home/YoutubeHeroSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useGSAP } from "@gsap/react";
import { YouTubeEmbed } from "@next/third-parties/google";
import gsap from "gsap";
import { ChevronDown, ChevronRight } from "lucide-react";
import Link from "next/link";
import { useRef } from "react";

import { useFontLoadState } from "@/shared/hooks/useFontLoadState";

import styles from "./YoutubeHeroSection.module.scss";

export const YoutubeHeroSection = () => {
const titleRef = useRef<HTMLHeadingElement>(null);
const textRef = useRef<HTMLDivElement>(null);
const registerButtonRef = useRef<HTMLButtonElement>(null);
const { fontLoaded } = useFontLoadState();

useGSAP(() => {
if (!fontLoaded) return;

const timeline = gsap.timeline();
timeline.fromTo(titleRef.current, { opacity: 0, y: 50 }, { opacity: 1, y: 0, duration: 2, ease: "expo.out" });
}, [fontLoaded]);
Comment on lines +18 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Guard null ref and verify useGSAP dependency signature

Prevent null target errors and use the current @gsap/react signature that uses an options object for dependencies (if on v2+).

-    useGSAP(() => {
-        if (!fontLoaded) return;
-
-        const timeline = gsap.timeline();
-        timeline.fromTo(titleRef.current, { opacity: 0, y: 50 }, { opacity: 1, y: 0, duration: 2, ease: "expo.out" });
-    }, [fontLoaded]);
+    useGSAP(() => {
+        if (!fontLoaded || !titleRef.current) return;
+        gsap.timeline().fromTo(
+            titleRef.current,
+            { opacity: 0, y: 50 },
+            { opacity: 1, y: 0, duration: 2, ease: "expo.out" }
+        );
+    }, { dependencies: [fontLoaded] });

If you’re on an older @gsap/react, keep the array deps and only add the null-guard.

To confirm plugin version and whether useGSAP is registered globally:


🏁 Script executed:

#!/bin/bash
# Show @gsap/react version and check for registration
fd -a package.json | xargs -I {} jq -r '.dependencies["@gsap/react"] // .devDependencies["@gsap/react"] // empty + " (file: {})"' {}
rg -nP 'registerPlugin\(\s*useGSAP\s*\)' -C2 --type ts --type tsx

Length of output: 241


Guard null refs and migrate to v2 useGSAP signature

 useGSAP(() => {
-    if (!fontLoaded) return;
-
-    const timeline = gsap.timeline();
-    timeline.fromTo(titleRef.current, { opacity: 0, y: 50 }, { opacity: 1, y: 0, duration: 2, ease: "expo.out" });
-}, [fontLoaded]);
+    if (!fontLoaded || !titleRef.current) return;
+    gsap.timeline().fromTo(
+        titleRef.current,
+        { opacity: 0, y: 50 },
+        { opacity: 1, y: 0, duration: 2, ease: "expo.out" }
+    );
+}, { dependencies: [fontLoaded] });

Aligns with @gsap/react 2.1.2’s object‐based deps API and prevents null‐target errors.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useGSAP(() => {
if (!fontLoaded) return;
const timeline = gsap.timeline();
timeline.fromTo(titleRef.current, { opacity: 0, y: 50 }, { opacity: 1, y: 0, duration: 2, ease: "expo.out" });
}, [fontLoaded]);
useGSAP(() => {
if (!fontLoaded || !titleRef.current) return;
gsap.timeline().fromTo(
titleRef.current,
{ opacity: 0, y: 50 },
{ opacity: 1, y: 0, duration: 2, ease: "expo.out" }
);
}, { dependencies: [fontLoaded] });
🤖 Prompt for AI Agents
In mosu-app/src/widgets/home/YoutubeHeroSection.tsx around lines 18 to 23,
update the useGSAP invocation to the v2 object-based signature and guard against
null refs: change the hook call to useGSAP({ callback: () => { if (!fontLoaded
|| !titleRef.current) return; const timeline = gsap.timeline();
timeline.fromTo(titleRef.current, { opacity: 0, y: 50 }, { opacity: 1, y: 0,
duration: 2, ease: "expo.out" }); }, deps: [fontLoaded] }); ensuring you check
titleRef.current exists before calling fromTo so no null-target errors occur.


return (
<section className={styles.yt_hero_section__wrapper}>
<h1 className={styles.yt_hero_section__title} ref={titleRef} style={{ opacity: fontLoaded ? 1 : 0 }}>
<span>당신의 수능을 바꿀&nbsp;</span>
<span>마지막 기회</span>
</h1>

<article className={styles.yt_hero_section__video}>
<YouTubeEmbed videoid={"sy0h73_DO5M"} style="margin: 0px auto;" />
</article>
Comment on lines +32 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix invalid React style prop (string passed instead of object)

Passing a string to style will error; use a style object.

-            <article className={styles.yt_hero_section__video}>
-                <YouTubeEmbed videoid={"sy0h73_DO5M"} style="margin: 0px auto;" />
-            </article>
+            <article className={styles.yt_hero_section__video}>
+                <YouTubeEmbed
+                    videoid="sy0h73_DO5M"
+                    style={{ margin: "0 auto" }}
+                />
+            </article>

Optional hardening (keeps viewers on your content):

+                <YouTubeEmbed
+                    videoid="sy0h73_DO5M"
+                    style={{ margin: "0 auto" }}
+                    params="rel=0&modestbranding=1&playsinline=1"
+                />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<article className={styles.yt_hero_section__video}>
<YouTubeEmbed videoid={"sy0h73_DO5M"} style="margin: 0px auto;" />
</article>
<article className={styles.yt_hero_section__video}>
<YouTubeEmbed
videoid="sy0h73_DO5M"
style={{ margin: "0 auto" }}
/>
</article>
🤖 Prompt for AI Agents
In mosu-app/src/widgets/home/YoutubeHeroSection.tsx around lines 32 to 34, the
YouTubeEmbed component is being passed a string for the style prop which will
error in React; change the prop to a proper style object (e.g., an object with
margin set to "0px auto") or remove the prop if unnecessary, and ensure the prop
type matches React.CSSProperties if the component is typed.


<div ref={textRef} className={styles.yt_hero_section__content} style={{ opacity: fontLoaded ? 1 : 0 }}>
<p>
<span>모의가 아닌 진짜 수능 이용 가이드입니다.</span>
</p>

<Link href="/apply" scroll>
<button ref={registerButtonRef}>
<span>지금 바로 신청하기</span>
<span>
<ChevronRight strokeWidth={1} />
</span>
</button>
</Link>
Comment on lines +41 to +48
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid interactive-in-interactive: don’t nest a button inside Link

Anchor-with-button is invalid and harms a11y. Make the Link’s anchor the CTA.

-                <Link href="/apply" scroll>
-                    <button ref={registerButtonRef}>
-                        <span>지금 바로 신청하기</span>
-                        <span>
-                            <ChevronRight strokeWidth={1} />
-                        </span>
-                    </button>
-                </Link>
+                <Link href="/apply" scroll>
+                    <span>지금 바로 신청하기</span>
+                    <span>
+                        <ChevronRight strokeWidth={1} />
+                    </span>
+                </Link>

Pair with the SCSS change that styles a { ... } instead of button { ... }.

🤖 Prompt for AI Agents
In mosu-app/src/widgets/home/YoutubeHeroSection.tsx around lines 41–48, a
<button> is nested inside a Next.js <Link>, which creates an interactive element
inside another interactive element; replace the inner <button> with an anchor
element used as the CTA (so the Link's anchor is the interactive element), move
the existing ref (registerButtonRef) from the button to the anchor, preserve the
inner spans/content as-is, ensure the anchor gets the same className/CSS hook
and any ARIA attributes needed (or add an aria-label) so accessibility/keyboard
focus is preserved, and update the SCSS to style the anchor selector instead of
button.


<footer className={styles.yt_hero_section__footer}>
<ChevronDown size={30} />
</footer>
Comment on lines +50 to +52
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

CSS module class not defined

styles.yt_hero_section__footer isn’t declared in the SCSS. Either add that class in the stylesheet or remove the className here.

-                <footer className={styles.yt_hero_section__footer}>
+                <footer>
                     <ChevronDown size={30} />
                 </footer>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<footer className={styles.yt_hero_section__footer}>
<ChevronDown size={30} />
</footer>
<footer>
<ChevronDown size={30} />
</footer>
🤖 Prompt for AI Agents
In mosu-app/src/widgets/home/YoutubeHeroSection.tsx around lines 50 to 52, the
JSX uses styles.yt_hero_section__footer but that class is not defined in the
SCSS module; fix by either adding the yt_hero_section__footer definition to the
corresponding .module.scss with the desired styles (matching naming convention
and export) or remove the className prop from the footer element so it relies on
default styles—ensure imports remain consistent and run the build to verify no
CSS-module type errors.

</div>
</section>
);
};