diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 78deb8ec..cd9f4def 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,19 @@ -import DesignSystemDemo from '@/pages/DesignSystem' +import { lazy, Suspense } from 'react' +import HomePage from '@/pages/Home' + +const DesignSystemDemo = lazy(() => import('@/pages/DesignSystem')) export default function App() { - return + const path = + typeof window !== 'undefined' ? window.location.pathname : '/' + + if (path.startsWith('/design-system')) { + return ( + + + + ) + } + + return } diff --git a/frontend/src/app/styles/global.css b/frontend/src/app/styles/global.css index 8d56d56d..b2899d23 100644 --- a/frontend/src/app/styles/global.css +++ b/frontend/src/app/styles/global.css @@ -3,8 +3,11 @@ * - html/body 기본 * - 접근성 기본 (focus-visible, prefers-reduced-motion) * - * Google Fonts (Fira Sans Extra Condensed, Geist, Geist Mono, Inter) 는 - * `index.html` 의 `` 로 로드 (성능상 CSS @import url 보다 권장). + * 폰트는 `index.html` 의 `` 로 로드. + * - Pretendard Variable (한·영 본문/서브헤딩) + * - Bricolage Grotesque (디스플레이 헤딩) + * - Caveat (스크립트 강조) + * - Geist Mono (코드/모노) */ @layer base { @@ -26,8 +29,8 @@ h1, h2, h3 { font-family: var(--font-heading); color: var(--color-fg-strong); - text-transform: uppercase; margin: 0; + letter-spacing: -0.01em; } h4, h5, h6 { @@ -58,3 +61,55 @@ } } } + +@layer utilities { + /* Hero stagger animation */ + @keyframes hero-rise { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + @keyframes hero-pop { + from { + opacity: 0; + transform: translateY(10px) rotate(-3deg) scale(0.92); + } + to { + opacity: 1; + transform: translateY(0) rotate(-2deg) scale(1); + } + } + @keyframes laptop-rise { + from { + opacity: 0; + transform: translateY(24px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + @keyframes screen-glow { + 0%, 100% { opacity: 0.85; } + 50% { opacity: 1; } + } + @keyframes caret-blink { + 0%, 49% { opacity: 1; } + 50%, 100% { opacity: 0; } + } + + .anim-hero-rise { + animation: hero-rise 0.8s var(--ease-decelerate) both; + } + .anim-hero-pop { + animation: hero-pop 0.9s var(--ease-decelerate) both; + } + .anim-laptop-rise { + animation: laptop-rise 0.9s var(--ease-decelerate) both; + } +} diff --git a/frontend/src/app/styles/tokens.css b/frontend/src/app/styles/tokens.css index 549b97b5..e1a67264 100644 --- a/frontend/src/app/styles/tokens.css +++ b/frontend/src/app/styles/tokens.css @@ -96,15 +96,17 @@ * font-heading, font-subheading, font-sans, font-mono * ========================================================= */ --font-heading: - 'Fira Sans Extra Condensed', 'Pretendard Variable', system-ui, sans-serif; + 'Bricolage Grotesque', 'Pretendard Variable', system-ui, sans-serif; --font-display: - 'Fira Sans Extra Condensed', 'Pretendard Variable', system-ui, sans-serif; + 'Bricolage Grotesque', 'Pretendard Variable', system-ui, sans-serif; --font-subheading: - 'Geist', 'Pretendard Variable', system-ui, sans-serif; + 'Pretendard Variable', system-ui, sans-serif; --font-sans: - 'Inter', 'Pretendard Variable', system-ui, sans-serif; + 'Pretendard Variable', system-ui, sans-serif; --font-body: - 'Inter', 'Pretendard Variable', system-ui, sans-serif; + 'Pretendard Variable', system-ui, sans-serif; + --font-script: + 'Caveat', 'Pretendard Variable', system-ui, sans-serif; --font-mono: 'Geist Mono', ui-monospace, 'JetBrains Mono', Consolas, monospace; diff --git a/frontend/src/pages/Home/index.ts b/frontend/src/pages/Home/index.ts new file mode 100644 index 00000000..38c68496 --- /dev/null +++ b/frontend/src/pages/Home/index.ts @@ -0,0 +1 @@ +export { default } from './ui/HomePage' diff --git a/frontend/src/pages/Home/ui/HomePage.tsx b/frontend/src/pages/Home/ui/HomePage.tsx new file mode 100644 index 00000000..d5dcf027 --- /dev/null +++ b/frontend/src/pages/Home/ui/HomePage.tsx @@ -0,0 +1,18 @@ +// PR 2 (feature/home-layout) 단계의 HomePage.tsx +import { SiteNav } from '@/widgets/site-nav' +import { SiteFooter } from '@/widgets/site-footer' +// 아직 PR 3에서 만들 예정이므로 주석 처리! +// import { HomeHero } from '@/widgets/home-hero' + +export default function HomePage() { + return ( +
+ {/* 글로벌 레이아웃은 추후 분리 예정*/} + +
+ {/* TODO: 다음 PR에서 위젯들 추가 예정 */} +
+ +
+ ) +} \ No newline at end of file diff --git a/frontend/src/shared/lib/AsyncBoundary/AsyncBoundary.tsx b/frontend/src/shared/lib/async-boundary/AsyncBoundary.tsx similarity index 100% rename from frontend/src/shared/lib/AsyncBoundary/AsyncBoundary.tsx rename to frontend/src/shared/lib/async-boundary/AsyncBoundary.tsx diff --git a/frontend/src/shared/lib/AsyncBoundary/ErrorBoundary.tsx b/frontend/src/shared/lib/async-boundary/ErrorBoundary.tsx similarity index 78% rename from frontend/src/shared/lib/AsyncBoundary/ErrorBoundary.tsx rename to frontend/src/shared/lib/async-boundary/ErrorBoundary.tsx index 81e2d5a7..df2431f8 100644 --- a/frontend/src/shared/lib/AsyncBoundary/ErrorBoundary.tsx +++ b/frontend/src/shared/lib/async-boundary/ErrorBoundary.tsx @@ -1,23 +1,23 @@ import { Component, type ErrorInfo, type ReactNode } from 'react'; -interface Props { +interface ErrorBoundaryProps { children: ReactNode; fallback: ReactNode | ((props: { error: Error; reset: () => void }) => ReactNode); onReset?: () => void; } -interface State { +interface ErrorBoundaryState { hasError: boolean; error: Error | null; } -export class ErrorBoundary extends Component { - constructor(props: Props) { +export class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null }; } - static getDerivedStateFromError(error: Error): State { + static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } diff --git a/frontend/src/shared/lib/AsyncBoundary/index.ts b/frontend/src/shared/lib/async-boundary/index.ts similarity index 100% rename from frontend/src/shared/lib/AsyncBoundary/index.ts rename to frontend/src/shared/lib/async-boundary/index.ts diff --git a/frontend/src/widgets/site-footer/index.ts b/frontend/src/widgets/site-footer/index.ts new file mode 100644 index 00000000..291f5dd6 --- /dev/null +++ b/frontend/src/widgets/site-footer/index.ts @@ -0,0 +1 @@ +export { SiteFooter } from './ui/SiteFooter' diff --git a/frontend/src/widgets/site-footer/ui/SiteFooter.tsx b/frontend/src/widgets/site-footer/ui/SiteFooter.tsx new file mode 100644 index 00000000..d3ae1cec --- /dev/null +++ b/frontend/src/widgets/site-footer/ui/SiteFooter.tsx @@ -0,0 +1,113 @@ +// 단순 뷰 섹션 widgets 에선 굳이 나누지 않는게 좋다고 판단했습니다. +// 상수, 메세지 등 마찬가지 +const columns = [ + { + title: 'Company', + links: [ + { label: 'Home', href: '#top' }, + { label: 'About', href: '#quote' }, + { label: 'FAQ', href: '#faq' }, + { label: 'Team', href: '#quote' }, + ], + }, + { + title: 'Services', + links: [ + { label: 'Frontend Interview', href: '#services' }, + { label: 'Backend Interview', href: '#services' }, + { label: 'CS / Full Stack', href: '#services' }, + { label: 'Reports', href: '#services' }, + ], + }, + { + title: 'Other', + links: [ + { label: 'Design System', href: '/design-system' }, + { label: 'GitHub', href: '#' }, + { label: 'Privacy', href: '#' }, + { label: 'Get Started', href: '#cta' }, + ], + }, +] + +export function SiteFooter() { + return ( +
+
+
+

+ One smart step +

+ + Get Started + + → + + +
+ +
+ +
+
+
+ Stack Up +
+

+ IT 직군 멀티모달 AI 면접 시뮬레이터. GitHub 레포와 이력서를 분석해 + 개인 맞춤 면접과 음성·비언어적 피드백을 제공합니다. +

+
+ + +
+ +
+
+ © 2026 StackUp · CNU 종합설계. All rights reserved. +
+ +
+
+
+ ) +} diff --git a/frontend/src/widgets/site-nav/index.ts b/frontend/src/widgets/site-nav/index.ts new file mode 100644 index 00000000..9ebd3419 --- /dev/null +++ b/frontend/src/widgets/site-nav/index.ts @@ -0,0 +1 @@ +export { SiteNav } from './ui/SiteNav' diff --git a/frontend/src/widgets/site-nav/ui/SiteNav.tsx b/frontend/src/widgets/site-nav/ui/SiteNav.tsx new file mode 100644 index 00000000..fcc1a775 --- /dev/null +++ b/frontend/src/widgets/site-nav/ui/SiteNav.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react' + +const items = [ + { href: '#services', label: 'Services' }, + { href: '#quote', label: 'About' }, + { href: '#faq', label: 'FAQ' }, +] + +export function SiteNav() { + const [scrolled, setScrolled] = useState(false) + + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 8) + onScroll() + window.addEventListener('scroll', onScroll, { passive: true }) + return () => window.removeEventListener('scroll', onScroll) + }, []) + + return ( +
+
+ + Stack Up + + + + + +
+
+ ) +}