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
17 changes: 11 additions & 6 deletions frontend/src/pages/Home/ui/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
// 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 (
<div className="min-h-svh bg-bg text-fg">
{/* 글로벌 레이아웃은 추후 분리 예정*/}
<SiteNav />
<main>
{/* TODO: 다음 PR에서 위젯들 추가 예정 */}
<HomeHero />
<HomeServices />
<HomeQuote />
<HomeFaq />
<HomeCta />
</main>
<SiteFooter />
</div>
)
}
}
2 changes: 2 additions & 0 deletions frontend/src/shared/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useTypewriter } from './useTypewriter'
export type { UseTypewriterOptions } from './useTypewriter'
61 changes: 61 additions & 0 deletions frontend/src/shared/hooks/useTypewriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 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.cancelAnimationFrame(rafId)
}
}, [text, startDelayMs, stepMs, respectReducedMotion])

return { typed, done: typed.length >= text.length }
}
1 change: 1 addition & 0 deletions frontend/src/widgets/home-cta/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HomeCta } from './ui/HomeCta'
51 changes: 51 additions & 0 deletions frontend/src/widgets/home-cta/ui/HomeCta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export function HomeCta() {
return (
<section id="cta" className="bg-bg">
<div className="mx-auto max-w-content px-6 lg:px-12 pb-24 lg:pb-32">
<div className="relative overflow-hidden rounded-2xl">
<img
src="/last-section-get-started.png"
alt=""
aria-hidden
loading="lazy"
decoding="async"
className="absolute inset-0 w-full h-full object-cover"
/>
<div
aria-hidden
className="absolute inset-0"
style={{
background:
'linear-gradient(180deg, rgba(20,26,17,0.55) 0%, rgba(20,26,17,0.35) 50%, rgba(20,26,17,0.65) 100%)',
}}
/>

<div className="relative min-h-[360px] lg:min-h-[440px] flex flex-col items-center justify-center text-center px-6 py-20 lg:py-28">
<h2
className="font-heading font-extrabold uppercase text-white leading-[0.95] tracking-tight"
style={{ fontSize: 'clamp(40px, 5.5vw, 72px)' }}
>
Ready to level up?
</h2>
<p className="mt-5 text-white/80 text-rich max-w-xl">
지금 GitHub 계정만 연결하면, 30초 안에 첫 모의면접이 시작됩니다.
</p>

<a
href="#login"
className="mt-10 inline-flex items-center gap-2 pl-5 pr-2 py-2.5 rounded-pill bg-[#e6dfd4] text-sage-900 text-button hover:bg-white transition-colors duration-fast"
>
Get Started
<span
aria-hidden
className="inline-flex items-center justify-center w-6 h-6 rounded-pill bg-sage-900 text-white text-[11px]"
>
</span>
</a>
</div>
</div>
</div>
</section>
)
}
1 change: 1 addition & 0 deletions frontend/src/widgets/home-faq/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HomeFaq } from './ui/HomeFaq'
86 changes: 86 additions & 0 deletions frontend/src/widgets/home-faq/ui/HomeFaq.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section id="faq" className="bg-bg">
<div className="mx-auto max-w-content px-6 lg:px-12 pt-8 pb-24 lg:pt-12 lg:pb-32">
<p className="text-button font-mono uppercase tracking-[0.22em] text-fg-muted">
FAQ
</p>

<h2
className="mt-4 font-heading font-extrabold uppercase text-sage-900 leading-[0.95] tracking-tight"
style={{ fontSize: 'clamp(40px, 5vw, 64px)' }}
>
Questions, answered.
</h2>

<div className="mt-12 h-px bg-border" />

<div className="mt-10 grid gap-12 lg:grid-cols-12">
<div className="lg:col-span-4">
<p className="text-rich text-fg-strong/85 leading-relaxed max-w-sm">
여기에 없는 궁금증은 Contact 페이지에서 이어서 답해드려요.
</p>
<a
href="#cta"
className="mt-6 inline-flex items-center gap-2 pl-5 pr-2 py-2.5 rounded-pill bg-sage-800 text-white text-button hover:bg-sage-900 transition-colors duration-fast"
>
Contact
<span
aria-hidden
className="inline-flex items-center justify-center w-6 h-6 rounded-pill bg-white/15"
>
</span>
</a>
</div>

<ul className="lg:col-span-8">
{faqs.map((f) => (
<li key={f.q} className="border-b border-border first:border-t">
<details className="group">
<summary className="flex items-center justify-between gap-6 py-6 cursor-pointer list-none select-none">
<span className="text-fg-strong text-[20px] lg:text-[22px] font-medium leading-snug">
{f.q}
</span>
<span
aria-hidden
className="shrink-0 w-7 h-7 grid place-items-center text-fg-strong text-xl transition-transform duration-normal ease-standard group-open:rotate-45"
>
+
</span>
</summary>
<div className="pb-6 pr-12 text-body text-fg-strong/70 leading-relaxed max-w-3xl">
{f.a}
</div>
</details>
</li>
))}
</ul>
</div>
</div>
</section>
)
}
1 change: 1 addition & 0 deletions frontend/src/widgets/home-hero/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HomeHero } from './ui/HomeHero'
38 changes: 38 additions & 0 deletions frontend/src/widgets/home-hero/ui/HomeHero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Laptop } from './Laptop'
import { ScreenContent } from './ScreenContent'

export function HomeHero() {
return (
<section
id="top"
className="relative overflow-hidden"
style={{
background:
'radial-gradient(120% 80% at 50% 0%, #f1ece4 0%, #e9e8e7 60%, #e9e8e7 100%)',
}}
>
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-[0.18] mix-blend-multiply"
style={{
background:
'radial-gradient(60% 50% at 18% 30%, rgba(31,39,27,0.5) 0%, transparent 60%),' +
'radial-gradient(40% 35% at 82% 20%, rgba(31,39,27,0.35) 0%, transparent 60%)',
}}
/>

<div className="relative mx-auto max-w-content px-6 lg:px-12 pt-10 pb-12 lg:pt-14 lg:pb-16 flex flex-col items-center">
<Laptop>
<ScreenContent />
</Laptop>

<p className="anim-hero-rise mt-6 lg:mt-8 text-button font-mono uppercase tracking-[0.22em] text-fg-muted [animation-delay:2.2s]">
<span className="inline-flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-success" aria-hidden />
Phase 1 · MVP · 2026
</span>
</p>
</div>
</section>
)
}
59 changes: 59 additions & 0 deletions frontend/src/widgets/home-hero/ui/Laptop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ReactNode } from 'react'

export function Laptop({ children }: { children: ReactNode }) {
return (
<div
className="anim-laptop-rise relative w-full [animation-delay:0.05s]"
style={{
maxWidth: 'min(820px, calc((100svh - 240px) * 1.6))',
filter: 'drop-shadow(0 30px 50px rgba(31,39,27,0.22))',
}}
>
{/* Lid */}
<div
className="relative rounded-[20px] p-[9px]"
style={{
background: 'linear-gradient(180deg, #d8d6d2 0%, #c9c6c2 100%)',
boxShadow:
'inset 0 1px 0 rgba(255,255,255,0.6), 0 1px 2px rgba(31,39,27,0.15)',
}}
>
{/* Bezel */}
<div
className="relative rounded-[12px] p-[12px]"
style={{ background: 'linear-gradient(180deg, #1a1a1a 0%, #0c0c0c 100%)' }}
>
{/* Camera dot */}
<div className="absolute top-[5px] left-1/2 -translate-x-1/2 w-1.5 h-1.5 rounded-full bg-zinc-700" />

{/* Screen */}
<div
className="relative aspect-[16/10] rounded-[5px] overflow-hidden flex items-center justify-center px-6"
style={{
background:
'radial-gradient(120% 100% at 50% 0%, #f5f1ea 0%, #e8e3da 60%, #ddd6cc 100%)',
}}
>
{children}
</div>
</div>
</div>

{/* Base / hinge */}
<div className="relative mx-auto" style={{ width: '108%' }}>
<div
className="h-[12px] -mt-[2px] mx-auto rounded-b-[10px]"
style={{
background: 'linear-gradient(180deg, #c2bfba 0%, #a8a5a0 100%)',
boxShadow:
'inset 0 1px 0 rgba(255,255,255,0.4), 0 8px 12px -6px rgba(31,39,27,0.25)',
}}
/>
<div
className="absolute top-0 left-1/2 -translate-x-1/2 h-[4px] w-[18%] rounded-b-md"
style={{ background: '#8e8b86' }}
/>
</div>
</div>
)
}
Loading