Skip to content

Conversation

@toothlessdev
Copy link
Member

@toothlessdev toothlessdev commented Sep 6, 2025

✅ Linked Issue

Summary by CodeRabbit

  • New Features
    • Replaced the homepage hero with a YouTube-style section featuring an embedded video, animated title, and a clear CTA to apply.
    • Improved load experience by coordinating animations with font readiness for smoother visuals.
  • Style
    • Added responsive styles for the new hero section, including button and footer animations.
    • Removed an unused media query to streamline CSS.

@toothlessdev toothlessdev self-assigned this Sep 6, 2025
@vercel
Copy link

vercel bot commented Sep 6, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
mosu-client Ready Ready Preview Comment Sep 6, 2025 5:50am

@coderabbitai
Copy link

coderabbitai bot commented Sep 6, 2025

Walkthrough

Replaced the homepage HeroSection with a new YoutubeHeroSection that embeds a YouTube video. Added a new hook to detect font load state for coordinating GSAP animations. Introduced corresponding SCSS module for the new hero and removed an empty media query from the old HeroSection styles.

Changes

Cohort / File(s) Summary
Homepage integration
mosu-app/src/pages/index.tsx
Swap HeroSection for YoutubeHeroSection; update imports and render.
New YouTube hero component
mosu-app/src/widgets/home/YoutubeHeroSection.tsx, mosu-app/src/widgets/home/YoutubeHeroSection.module.scss
Add YouTube-embedded hero with animated title/content, CTA to /apply, and responsive styles.
Font loading hook
mosu-app/src/shared/hooks/useFontLoadState.ts
New hook returning { fontLoaded }, using document.fonts.ready with fallback timeout.
Legacy hero style cleanup
mosu-app/src/widgets/home/HeroSection.module.scss
Remove empty @media (max-width: 1023px) block.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant NextPage as pages/index.tsx
  participant YTHero as YoutubeHeroSection
  participant Hook as useFontLoadState
  participant Fonts as document.fonts
  participant GSAP as GSAP Timeline
  participant Router as Next Router

  User->>NextPage: Load "/"
  NextPage->>YTHero: Render component
  YTHero->>Hook: useFontLoadState()
  alt fonts API available
    Hook->>Fonts: await fonts.ready
    Fonts-->>Hook: resolved
  else fallback
    Hook-->>YTHero: timeout (~100ms)
  end
  Hook-->>YTHero: fontLoaded = true
  YTHero->>GSAP: Play intro animations (title/content)
  User->>YTHero: Click "지금 바로 상담받기" CTA
  YTHero->>Router: navigate to "/apply"
  Router-->>User: Apply page rendered
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
메인페이지에 YouTube 임베드 영상 삽입 (#385)

Assessment against linked issues: Out-of-scope changes

(None)

Suggested labels

기능 구현, 도메인 : 공통, 우선순위 (상)

Suggested reviewers

  • kimgho

Poem

A carrot-orange play button gleams so bright,
I thump my paws—our hero’s set just right!
Fonts load, titles rise, we bounce with cheer,
A tunnel to /apply appears so near.
With whiskered pride I nudge “Play” anew—
Behold! Our page now streams on YouTube. 🥕📺

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature#385

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Sep 6, 2025

Warnings
⚠️ PR에 Reviewers가 지정되어 있지 않습니다. 리뷰어를 지정해주세요.
Messages
📖 ✅ PR 제목에 이슈 번호가 포함되어 있습니다.
📖 ✅ PR에 라벨이 지정되어 있습니다.
📖 ✅ PR에 Assignees가 지정되어 있습니다.
📖 ✅ package.json에 변경사항이 없습니다.
📖 ✅ 브랜치 이름 'feature#385'이 컨벤션을 따릅니다.
📖 ✅ TypeScript 컴파일이 성공적으로 완료되었습니다.
📖 ✅ ESLint 검사 결과 문제가 없습니다.

📝 추가 및 변경된 파일

총 5개 파일 변경

└── 📂 mosu-app/
    └── 📂 src/
        ├── 📂 pages/
        │   └── ⚛️ index.tsx
        ├── 📂 widgets/
        │   └── 📂 home/
        │       ├── 📄 HeroSection.module.scss
        │       ├── 📄 YoutubeHeroSection.module.scss
        │       └── ⚛️ YoutubeHeroSection.tsx
        └── 📂 shared/
            └── 📂 hooks/
                └── 📘 useFontLoadState.ts

Generated by 🚫 dangerJS against b243af3

@github-actions
Copy link

github-actions bot commented Sep 6, 2025

📚 Storybook이 Chromatic에 배포되었습니다!

@toothlessdev toothlessdev merged commit 01e634a into main Sep 6, 2025
10 checks passed
@toothlessdev toothlessdev deleted the feature#385 branch September 6, 2025 05:52
@github-project-automation github-project-automation bot moved this from 진행중 to 완료 in mosu-client Sep 6, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (4)
mosu-app/src/shared/hooks/useFontLoadState.ts (1)

6-14: Avoid setState after unmount; add cleanup and timeout handle

Guard against unmount and clear the fallback timeout to prevent memory leaks and React warnings.

-    useEffect(() => {
-        if (document.fonts && document.fonts.ready) {
-            document.fonts.ready.then(() => {
-                setFontLoaded(true);
-            });
-        } else {
-            setTimeout(() => setFontLoaded(true), 100);
-        }
-    }, []);
+    useEffect(() => {
+        let mounted = true;
+        let timeoutId: number | null = null;
+        const markLoaded = () => mounted && setFontLoaded(true);
+
+        if (document.fonts && document.fonts.ready) {
+            document.fonts.ready.then(markLoaded);
+        } else {
+            timeoutId = window.setTimeout(markLoaded, 150);
+        }
+        return () => {
+            mounted = false;
+            if (timeoutId !== null) clearTimeout(timeoutId);
+        };
+    }, []);
mosu-app/src/widgets/home/YoutubeHeroSection.module.scss (1)

16-17: Provide fallback for overflow: clip

Older browsers may not support clip. Layer a hidden fallback.

-        overflow: clip;
+        overflow: hidden; /* fallback */
+        overflow: clip;
mosu-app/src/pages/index.tsx (1)

9-9: Remove commented-out code

Drop the commented import and component usage to keep the page clean.

-// import { HeroSection } from "@/widgets/home/HeroSection";
-{/* <HeroSection /> */}

Also applies to: 27-28

mosu-app/src/widgets/home/YoutubeHeroSection.tsx (1)

15-15: Remove unused ref

registerButtonRef is no longer used after converting the CTA to a Link-only anchor.

-    const registerButtonRef = useRef<HTMLButtonElement>(null);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65f8982 and b243af3.

📒 Files selected for processing (5)
  • mosu-app/src/pages/index.tsx (2 hunks)
  • mosu-app/src/shared/hooks/useFontLoadState.ts (1 hunks)
  • mosu-app/src/widgets/home/HeroSection.module.scss (0 hunks)
  • mosu-app/src/widgets/home/YoutubeHeroSection.module.scss (1 hunks)
  • mosu-app/src/widgets/home/YoutubeHeroSection.tsx (1 hunks)
💤 Files with no reviewable changes (1)
  • mosu-app/src/widgets/home/HeroSection.module.scss
🧰 Additional context used
🧬 Code graph analysis (2)
mosu-app/src/pages/index.tsx (1)
mosu-app/src/widgets/home/YoutubeHeroSection.tsx (1)
  • YoutubeHeroSection (12-56)
mosu-app/src/widgets/home/YoutubeHeroSection.tsx (1)
mosu-app/src/shared/hooks/useFontLoadState.ts (1)
  • useFontLoadState (3-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run DangerJS
  • GitHub Check: Run Unit & Integration Tests
🔇 Additional comments (4)
mosu-app/src/shared/hooks/useFontLoadState.ts (1)

3-17: Hook looks good overall

Client-only access via useEffect and a sane fallback path.

mosu-app/src/widgets/home/YoutubeHeroSection.module.scss (1)

9-12: Verify header/banner heights or derive from CSS vars

Hard-coding 100px/70px may drift from real header/banner sizes across breakpoints.

If you already expose header/banner heights as CSS variables, consider:

-        height: calc(100vh - 100px - 70px);
+        /* Example if --header-h and --banner-h are available */
+        height: calc(100vh - var(--header-h, 100px) - var(--banner-h, 70px));
mosu-app/src/pages/index.tsx (1)

16-16: New hero import looks good

Homepage now clearly sources the YouTube hero.

mosu-app/src/widgets/home/YoutubeHeroSection.tsx (1)

25-55: Overall structure and animation trigger look good

Title/content opacity gating on font load is sensible; timeline usage is clear.

Comment on lines +89 to +131
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;
}
}
Copy link

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 */
}
}

Comment on lines +18 to +23
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]);
Copy link

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.

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

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.

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

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.

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: 완료

Development

Successfully merging this pull request may close these issues.

[✨ 기능 요청] 메인페이지 youtube embed 영상 삽입

2 participants