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 (
+
+ )
+}
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 (
+
+ )
+}