From 8e5692488c12eb575c31beac37cd9d1fb37c0bda Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 13 May 2026 00:32:13 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat=20:=20=ED=83=80=EC=9D=B4=ED=95=91=20?= =?UTF-8?q?=ED=9A=A8=EA=B3=BC=20=ED=9B=85=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/shared/hooks/index.ts | 2 + frontend/src/shared/hooks/useTypewriter.ts | 52 ++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 frontend/src/shared/hooks/useTypewriter.ts diff --git a/frontend/src/shared/hooks/index.ts b/frontend/src/shared/hooks/index.ts index e69de29b..f0e17fce 100644 --- a/frontend/src/shared/hooks/index.ts +++ b/frontend/src/shared/hooks/index.ts @@ -0,0 +1,2 @@ +export { useTypewriter } from './useTypewriter' +export type { UseTypewriterOptions } from './useTypewriter' diff --git a/frontend/src/shared/hooks/useTypewriter.ts b/frontend/src/shared/hooks/useTypewriter.ts new file mode 100644 index 00000000..5c1635d2 --- /dev/null +++ b/frontend/src/shared/hooks/useTypewriter.ts @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react' + +export type UseTypewriterOptions = { + startDelayMs?: number + stepMs?: number + respectReducedMotion?: boolean +} + +export function useTypewriter( + text: string, + options: UseTypewriterOptions = {}, +) { + const { + startDelayMs = 0, + stepMs = 100, + respectReducedMotion = true, + } = options + + const [typed, setTyped] = useState('') + + useEffect(() => { + const reduce = + respectReducedMotion && + typeof window !== 'undefined' && + window.matchMedia?.('(prefers-reduced-motion: reduce)').matches + + if (reduce) { + setTyped(text) + return + } + + setTyped('') + let intervalId: number | undefined + const timeoutId = window.setTimeout(() => { + let i = 0 + intervalId = window.setInterval(() => { + i += 1 + setTyped(text.slice(0, i)) + if (i >= text.length && intervalId !== undefined) { + window.clearInterval(intervalId) + } + }, stepMs) + }, startDelayMs) + + return () => { + window.clearTimeout(timeoutId) + if (intervalId !== undefined) window.clearInterval(intervalId) + } + }, [text, startDelayMs, stepMs, respectReducedMotion]) + + return { typed, done: typed.length >= text.length } +} From 91e7725e124a8b09e8c566b1f2b065f184d6c748 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 13 May 2026 00:33:05 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat=20:=20=ED=99=88=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=201=EB=B2=88=20Cta=EC=84=B9=EC=85=98=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/widgets/home-cta/index.ts | 1 + frontend/src/widgets/home-cta/ui/HomeCta.tsx | 51 ++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 frontend/src/widgets/home-cta/index.ts create mode 100644 frontend/src/widgets/home-cta/ui/HomeCta.tsx diff --git a/frontend/src/widgets/home-cta/index.ts b/frontend/src/widgets/home-cta/index.ts new file mode 100644 index 00000000..408926af --- /dev/null +++ b/frontend/src/widgets/home-cta/index.ts @@ -0,0 +1 @@ +export { HomeCta } from './ui/HomeCta' diff --git a/frontend/src/widgets/home-cta/ui/HomeCta.tsx b/frontend/src/widgets/home-cta/ui/HomeCta.tsx new file mode 100644 index 00000000..953fca4f --- /dev/null +++ b/frontend/src/widgets/home-cta/ui/HomeCta.tsx @@ -0,0 +1,51 @@ +export function HomeCta() { + return ( +
+
+
+ +
+ +
+

+ Ready to level up? +

+

+ 지금 GitHub 계정만 연결하면, 30초 안에 첫 모의면접이 시작됩니다. +

+ + + Get Started + + → + + +
+
+
+
+ ) +} From edd82255fe7d529323123dde323ada0e173f8475 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 13 May 2026 00:34:08 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat=20:=20Laptop=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/widgets/home-hero/ui/Laptop.tsx | 59 ++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 frontend/src/widgets/home-hero/ui/Laptop.tsx diff --git a/frontend/src/widgets/home-hero/ui/Laptop.tsx b/frontend/src/widgets/home-hero/ui/Laptop.tsx new file mode 100644 index 00000000..a0c505a0 --- /dev/null +++ b/frontend/src/widgets/home-hero/ui/Laptop.tsx @@ -0,0 +1,59 @@ +import type { ReactNode } from 'react' + +export function Laptop({ children }: { children: ReactNode }) { + return ( +
+ {/* Lid */} +
+ {/* Bezel */} +
+ {/* Camera dot */} +
+ + {/* Screen */} +
+ {children} +
+
+
+ + {/* Base / hinge */} +
+
+
+
+
+ ) +} From 69b727bf2a08ee7d5ede55b3c60d089d7851e871 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 13 May 2026 00:34:26 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat=20:=20=EC=8A=A4=ED=81=AC=EB=A6=B0=20?= =?UTF-8?q?=EC=BB=A8=ED=85=90=EC=B8=A0=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/home-hero/ui/ScreenContent.tsx | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 frontend/src/widgets/home-hero/ui/ScreenContent.tsx diff --git a/frontend/src/widgets/home-hero/ui/ScreenContent.tsx b/frontend/src/widgets/home-hero/ui/ScreenContent.tsx new file mode 100644 index 00000000..0d50d1fb --- /dev/null +++ b/frontend/src/widgets/home-hero/ui/ScreenContent.tsx @@ -0,0 +1,60 @@ +import { useTypewriter } from '@/shared/hooks' + +const TYPED_TEXT = 'Stack Up' + +export function ScreenContent() { + const { typed, done } = useTypewriter(TYPED_TEXT, { + startDelayMs: 850, + stepMs: 130, + }) + + return ( +
+

+ IT Interview Solution +

+ +

+ {typed} + +

+ + +
+ ) +} From 554136a5a0cbfbf1f47542c0b0b28f2d5d8a1477 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 13 May 2026 00:34:47 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat=20:=20=ED=99=88=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=ED=9E=88=EC=96=B4=EB=A1=9C=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/widgets/home-hero/index.ts | 1 + .../src/widgets/home-hero/ui/HomeHero.tsx | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 frontend/src/widgets/home-hero/index.ts create mode 100644 frontend/src/widgets/home-hero/ui/HomeHero.tsx diff --git a/frontend/src/widgets/home-hero/index.ts b/frontend/src/widgets/home-hero/index.ts new file mode 100644 index 00000000..ec86fcb7 --- /dev/null +++ b/frontend/src/widgets/home-hero/index.ts @@ -0,0 +1 @@ +export { HomeHero } from './ui/HomeHero' diff --git a/frontend/src/widgets/home-hero/ui/HomeHero.tsx b/frontend/src/widgets/home-hero/ui/HomeHero.tsx new file mode 100644 index 00000000..40e4ecb1 --- /dev/null +++ b/frontend/src/widgets/home-hero/ui/HomeHero.tsx @@ -0,0 +1,38 @@ +import { Laptop } from './Laptop' +import { ScreenContent } from './ScreenContent' + +export function HomeHero() { + return ( +
+
+ +
+ + + + +

+ + + Phase 1 · MVP · 2026 + +

+
+
+ ) +} From 36a08fe08406f09100915633f8e456eaa648da3e Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 13 May 2026 00:35:11 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat=20:=20=ED=99=88=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/widgets/home-services/index.ts | 1 + .../widgets/home-services/ui/HomeServices.tsx | 80 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 frontend/src/widgets/home-services/index.ts create mode 100644 frontend/src/widgets/home-services/ui/HomeServices.tsx diff --git a/frontend/src/widgets/home-services/index.ts b/frontend/src/widgets/home-services/index.ts new file mode 100644 index 00000000..b58cf390 --- /dev/null +++ b/frontend/src/widgets/home-services/index.ts @@ -0,0 +1 @@ +export { HomeServices } from './ui/HomeServices' diff --git a/frontend/src/widgets/home-services/ui/HomeServices.tsx b/frontend/src/widgets/home-services/ui/HomeServices.tsx new file mode 100644 index 00000000..e19a1334 --- /dev/null +++ b/frontend/src/widgets/home-services/ui/HomeServices.tsx @@ -0,0 +1,80 @@ +type ServiceCard = { + tag: string + title: string + image: string +} + +const services: ServiceCard[] = [ + { + tag: 'Service', + title: 'Frontend Interview', + image: '/second-section-frontend-interview.png', + }, + { + tag: 'Service', + title: 'Backend Interview', + image: '/second-section-backend-interview.avif', + }, + { + tag: 'Service', + title: 'CS / Full Stack', + image: '/second-section-cs-interview.avif', + }, +] + +export function HomeServices() { + return ( +
+
+

+ Our Services +

+ + +
+ ) +} From e0aa8f15a71a7c26b02002500b103817a6a30bd6 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 13 May 2026 00:35:42 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat=20:=20=ED=99=88=20Quote=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/widgets/home-quote/index.ts | 1 + .../src/widgets/home-quote/ui/HomeQuote.tsx | 88 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 frontend/src/widgets/home-quote/index.ts create mode 100644 frontend/src/widgets/home-quote/ui/HomeQuote.tsx diff --git a/frontend/src/widgets/home-quote/index.ts b/frontend/src/widgets/home-quote/index.ts new file mode 100644 index 00000000..e685ef69 --- /dev/null +++ b/frontend/src/widgets/home-quote/index.ts @@ -0,0 +1 @@ +export { HomeQuote } from './ui/HomeQuote' diff --git a/frontend/src/widgets/home-quote/ui/HomeQuote.tsx b/frontend/src/widgets/home-quote/ui/HomeQuote.tsx new file mode 100644 index 00000000..e27dba30 --- /dev/null +++ b/frontend/src/widgets/home-quote/ui/HomeQuote.tsx @@ -0,0 +1,88 @@ +export function HomeQuote() { + return ( +
+
+
+
+ {/* decorative left panel */} +
+
+
+
+
+ Stack Up +
+
+
+ + {/* quote panel */} +
+
+ “ +
+ +
+

+ Stack Up은 올인원 IT 면접 솔루션입니다. 본인 코드를 가장 잘 아는 + 면접관 — GitHub 레포에서 출발해, 답변의 깊이까지 따라 들어갑니다. +

+
+ +
+
+
+ 박상우 · 신재호 · 정준모 · 조서현 +
+
+ Team StackUp · CNU +
+
+
+ + + 2026 · Phase 1 + +
+
+
+
+
+
+
+ ) +} From 07d27eb81148ff9f425de90802a4f7e78203e7d9 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 13 May 2026 00:36:05 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat=20:=20=ED=99=88=20faq=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/widgets/home-faq/index.ts | 1 + frontend/src/widgets/home-faq/ui/HomeFaq.tsx | 86 ++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 frontend/src/widgets/home-faq/index.ts create mode 100644 frontend/src/widgets/home-faq/ui/HomeFaq.tsx diff --git a/frontend/src/widgets/home-faq/index.ts b/frontend/src/widgets/home-faq/index.ts new file mode 100644 index 00000000..f9e10fb2 --- /dev/null +++ b/frontend/src/widgets/home-faq/index.ts @@ -0,0 +1 @@ +export { HomeFaq } from './ui/HomeFaq' diff --git a/frontend/src/widgets/home-faq/ui/HomeFaq.tsx b/frontend/src/widgets/home-faq/ui/HomeFaq.tsx new file mode 100644 index 00000000..ab517cff --- /dev/null +++ b/frontend/src/widgets/home-faq/ui/HomeFaq.tsx @@ -0,0 +1,86 @@ +const faqs = [ + { + q: 'Stack Up은 무료 크레딧을 제공하나요?', + a: '월 2회의 무료 면접 세션을 제공합니다. 학교 종합설계 프로젝트 기준이라 결제·구독 기능은 포함되지 않아요.', + }, + { + q: '어떤 형식의 이력서를 지원하나요?', + a: 'Phase 1 기준 PDF만 지원합니다. HWP·DOCX는 PDF로 변환 후 업로드해 주세요.', + }, + { + q: 'GitHub 외 다른 로그인은 가능한가요?', + a: '레포 분석이 핵심 기능이라, Phase 1에서는 GitHub OAuth만 제공합니다.', + }, + { + q: '꼬리질문은 얼마나 빨리 받을 수 있나요?', + a: '평균 3초 이내 응답을 목표로 합니다. Flash 모델과 사전 구축 RAG 인덱스로 지연을 최소화해요.', + }, + { + q: '음성·비언어 분석은 어떻게 동작하나요?', + a: 'WebRTC로 마이크·웹캠 스트림을 받아 말 속도(WPM)·간투어·시선·자세를 분석합니다. 권한을 거부하면 텍스트 입력으로 진행할 수 있어요.', + }, +] + +export function HomeFaq() { + return ( +
+
+

+ FAQ +

+ +

+ Questions, answered. +

+ +
+ +
+
+

+ 여기에 없는 궁금증은 Contact 페이지에서 이어서 답해드려요. +

+ + Contact + + → + + +
+ +
    + {faqs.map((f) => ( +
  • +
    + + + {f.q} + + + + + + +
    + {f.a} +
    +
    +
  • + ))} +
+
+
+
+ ) +} From f809a5258cb2b8da82fce3bd961f526307c80c80 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 13 May 2026 00:47:13 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat=20:=20HomePage=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=A1=B0=EB=A6=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Home/ui/HomePage.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/Home/ui/HomePage.tsx b/frontend/src/pages/Home/ui/HomePage.tsx index d5dcf027..365a73c0 100644 --- a/frontend/src/pages/Home/ui/HomePage.tsx +++ b/frontend/src/pages/Home/ui/HomePage.tsx @@ -1,16 +1,21 @@ -// PR 2 (feature/home-layout) 단계의 HomePage.tsx import { SiteNav } from '@/widgets/site-nav' +import { HomeHero } from '@/widgets/home-hero' +import { HomeServices } from '@/widgets/home-services' +import { HomeQuote } from '@/widgets/home-quote' +import { HomeFaq } from '@/widgets/home-faq' +import { HomeCta } from '@/widgets/home-cta' import { SiteFooter } from '@/widgets/site-footer' -// 아직 PR 3에서 만들 예정이므로 주석 처리! -// import { HomeHero } from '@/widgets/home-hero' export default function HomePage() { return (
- {/* 글로벌 레이아웃은 추후 분리 예정*/}
- {/* TODO: 다음 PR에서 위젯들 추가 예정 */} + + + + +
From 0d02f65fb22fd527e3c0c1ac0512601248af149f Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 13 May 2026 15:43:25 +0900 Subject: [PATCH 10/10] =?UTF-8?q?refactor=20:=20setInterval=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20requestAnimationFrame=20=EB=B0=A9=EC=8B=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/shared/hooks/useTypewriter.ts | 35 ++++++++++++++-------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/frontend/src/shared/hooks/useTypewriter.ts b/frontend/src/shared/hooks/useTypewriter.ts index 5c1635d2..d7f36398 100644 --- a/frontend/src/shared/hooks/useTypewriter.ts +++ b/frontend/src/shared/hooks/useTypewriter.ts @@ -30,21 +30,30 @@ export function useTypewriter( } setTyped('') - let intervalId: number | undefined - const timeoutId = window.setTimeout(() => { - let i = 0 - intervalId = window.setInterval(() => { - i += 1 - setTyped(text.slice(0, i)) - if (i >= text.length && intervalId !== undefined) { - window.clearInterval(intervalId) - } - }, stepMs) - }, startDelayMs) + let rafId = 0 + let startTime: number | null = null + + const tick = (now: number) => { + if (startTime === null) startTime = now + const elapsed = now - startTime - startDelayMs + if (elapsed < 0) { + rafId = window.requestAnimationFrame(tick) + return + } + const next = Math.min( + Math.floor(elapsed / stepMs) + 1, + text.length, + ) + setTyped(text.slice(0, next)) + if (next < text.length) { + rafId = window.requestAnimationFrame(tick) + } + } + + rafId = window.requestAnimationFrame(tick) return () => { - window.clearTimeout(timeoutId) - if (intervalId !== undefined) window.clearInterval(intervalId) + window.cancelAnimationFrame(rafId) } }, [text, startDelayMs, stepMs, respectReducedMotion])