diff --git a/docs/design-system.md b/docs/design-system.md index 2b3fd549..7b4f4687 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -12,7 +12,7 @@ 1. **신뢰감 우선** — 면접 도구이므로 가벼운 톤 지양. 진중·차분한 시각 언어. 2. **집중 환경 보호** — 면접 화면은 노이즈 최소화. 핵심 UI만 노출, 부가 정보는 hover/click 으로 점진적 공개. 3. **상태 가시성** — 분석 진행, 세션 상태, SSE 이벤트 등 비동기 상태는 항상 명시적으로 표현. -4. **모노크로매틱 + 의미 컬러** — 로고 블루(#3E63D8) 기반 **쿨 블루그레이** 단일 베이스(토큰명은 `sage-*` 유지)로 톤 일관성을 유지하고, 브랜드 강조는 로고 블루 `primary`, Status / Domain 만 muted jewel tone 으로 식별성 부여. +4. **모노크로매틱 + 의미 컬러** — 브랜드 블루(#4662D2) 기반 **쿨 블루그레이** 단일 베이스(토큰명은 `sage-*` 유지)로 톤 일관성을 유지하고, 브랜드 강조는 로고 블루 `primary`, Status / Domain 만 muted jewel tone 으로 식별성 부여. 5. **접근성 (WCAG 2.1 AA)** — 키보드 only 조작, 명도 대비 4.5:1 이상, focus ring 명확. 6. **모바일 웹뷰 대응** — 데스크탑 우선이지만 mobile breakpoint(`< lg`)에서도 깨지지 않게. @@ -32,7 +32,7 @@ ### 2.2 컬러 — Sage Scale (메인 테마, 11 단계) -> 토큰 이름은 `sage-*` 로 유지하되 **값은 로고(#3E63D8)에 맞춘 쿨 블루그레이 모노크롬** 이다(저채도 — 중립/본문용). 브랜드 강조색은 별도 `primary` (아래 §2.3, 스케일과 분리). +> 토큰 이름은 `sage-*` 로 유지하되 **값은 브랜드 블루(#4662D2)에 맞춘 쿨 블루그레이 모노크롬** 이다(저채도 — 중립/본문용). 브랜드 강조색은 별도 `primary` (아래 §2.3, 스케일과 분리). | 토큰 | Hex | 권장 용도 | |---|---|---| @@ -67,9 +67,12 @@ Tailwind 사용: `bg-sage-{n}`, `text-sage-{n}`, `border-sage-{n}`. | `--color-fg-subtle` | `sage-400` | `text-fg-subtle` | | `--color-fg-disabled` | `sage-200` | `text-fg-disabled` | | `--color-fg-on-primary` | `white` | `text-fg-on-primary` | -| `--color-primary` | `#3e63d8` (로고 블루, 스케일과 분리) | `bg-primary`, `text-primary` | -| `--color-primary-hover` | `#3050be` | `bg-primary-hover` | -| `--color-primary-pressed` | `#274099` | `bg-primary-pressed` | +| `--color-primary` | `#4662d2` (브랜드 블루, 스케일과 분리) | `bg-primary`, `text-primary` | +| `--color-primary-hover` | `#3b54bd` | `bg-primary-hover` | +| `--color-primary-pressed` | `#31479f` | `bg-primary-pressed` | +| `--color-primary-50` | `#eff2fd` (연한 파랑 표면) | `bg-primary-50` | +| `--color-primary-100` | `#dee4fa` | `bg-primary-100` | +| `--color-primary-200` | `#c3cdf5` (틴트 위 보더) | `border-primary-200` | ### 2.4 컬러 — Status @@ -412,7 +415,7 @@ Tailwind v4 기본 `--spacing: 0.25rem` (= 4px) 사용. `p-4` = `16px`. ## 12. 변경 이력 -- **2026-06** — **블루 리테마**: 로고(#3E63D8)에 맞춰 `sage-*` 스케일 값을 쿨 블루그레이 모노크롬으로 교체(토큰명 유지), `primary` 를 스케일에서 분리해 로고 블루로 지정. 배경/그림자/포커스링도 쿨톤·네이비로 재조정. 위젯 하드코딩 hex 일괄 치환. Status / Domain 컬러는 유지. +- **2026-06** — **블루 리테마**: 브랜드 블루(#4662D2)에 맞춰 `sage-*` 스케일 값을 쿨 블루그레이 모노크롬으로 교체(토큰명 유지), `primary` 를 스케일에서 분리해 로고 블루로 지정. 배경/그림자/포커스링도 쿨톤·네이비로 재조정. 위젯 하드코딩 hex 일괄 치환. Status / Domain 컬러는 유지. - **2026-05** — Tailwind CSS v4 기반 **sage 모노크로매틱** 시스템으로 전면 개편. 이전 `Pretendard + 블루 브랜드` 스펙은 본 문서로 흡수. - **2026-05** — Status / Domain 컬러를 muted jewel tone 으로 재정의 (Sage 톤과 조화). - **2026-05** — Z-index, Container max-width, 4-State UI 패턴 등 누락 토큰 / 규약 보강. diff --git a/frontend/index.html b/frontend/index.html index 02a6296f..8c75c813 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -15,7 +15,7 @@ /> StackUp — AI Interview Solution diff --git a/frontend/public/last-section-get-started.png b/frontend/public/last-section-get-started.png deleted file mode 100644 index 44728d4f..00000000 Binary files a/frontend/public/last-section-get-started.png and /dev/null differ diff --git a/frontend/public/second-section-backend-interview.avif b/frontend/public/second-section-backend-interview.avif deleted file mode 100644 index aa31614f..00000000 Binary files a/frontend/public/second-section-backend-interview.avif and /dev/null differ diff --git a/frontend/public/second-section-cs-interview.webp b/frontend/public/second-section-cs-interview.webp deleted file mode 100644 index 234f1afb..00000000 Binary files a/frontend/public/second-section-cs-interview.webp and /dev/null differ diff --git a/frontend/public/second-section-frontend-interview.png b/frontend/public/second-section-frontend-interview.png deleted file mode 100644 index 0772b77a..00000000 Binary files a/frontend/public/second-section-frontend-interview.png and /dev/null differ diff --git a/frontend/src/app/styles/global.css b/frontend/src/app/styles/global.css index 57063994..48568970 100644 --- a/frontend/src/app/styles/global.css +++ b/frontend/src/app/styles/global.css @@ -6,7 +6,6 @@ * 폰트는 `index.html` 의 `` 로 로드. * - Pretendard Variable (한·영 본문/서브헤딩) * - Bricolage Grotesque (디스플레이 헤딩) - * - Caveat (스크립트 강조) * - Geist Mono (코드/모노) */ diff --git a/frontend/src/app/styles/tokens.css b/frontend/src/app/styles/tokens.css index bb6ddb40..994bb5c5 100644 --- a/frontend/src/app/styles/tokens.css +++ b/frontend/src/app/styles/tokens.css @@ -16,7 +16,7 @@ /* ========================================================= * 2. Color — Sage / Neutral Scale (Main Theme) * bg-sage-500, text-sage-800, border-sage-200 ... - * NOTE: 토큰 이름은 'sage' 로 유지하되 값은 로고(#3E63D8)에 맞춘 + * NOTE: 토큰 이름은 'sage' 로 유지하되 값은 브랜드 블루(#4662D2)에 맞춘 * 쿨 블루그레이 모노크롬 램프다(저채도 — 본문/중립용). 브랜드 강조색은 §3 primary. * ========================================================= */ --color-sage-50: #eef1f6; @@ -51,11 +51,18 @@ --color-fg-disabled: var(--color-sage-200); --color-fg-on-primary: var(--color-white); - /* 브랜드 강조색 — 로고 블루(#3E63D8). 중립 스케일과 분리(고채도라 본문엔 미사용, - * 버튼·링크·강조에만). bg-primary / text-primary / border-primary 가 이 값을 쓴다. */ - --color-primary: #3e63d8; - --color-primary-hover: #3050be; - --color-primary-pressed: #274099; + /* 브랜드 강조색 — #4662D2. 중립 스케일과 분리(고채도라 본문엔 미사용, + * 버튼·링크·강조에만). bg-primary / text-primary / border-primary 가 이 값을 쓴다. + * 흰 글자 대비 5.33:1 — 일반 텍스트 AA 통과. */ + --color-primary: #4662d2; + --color-primary-hover: #3b54bd; + --color-primary-pressed: #31479f; + + /* primary 틴트 — 연한 파랑 표면/보더용. 랜딩의 강조 블록, 뱃지 배경에 사용. + * 본문 텍스트 배경으로 쓸 때 fg(#101627) 대비 충분. */ + --color-primary-50: #eff2fd; + --color-primary-100: #dee4fa; + --color-primary-200: #c3cdf5; /* ========================================================= * 4. Color — Status (Success / Warning / Danger / Info) @@ -112,8 +119,6 @@ 'Pretendard Variable', system-ui, sans-serif; --font-body: '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; @@ -214,7 +219,7 @@ --shadow-lg: 0 10px 15px -3px rgba(16, 22, 39, 0.10), 0 4px 6px -4px rgba(16, 22, 39, 0.05); - --shadow-focus-ring: 0 0 0 3px rgba(62, 99, 216, 0.45); + --shadow-focus-ring: 0 0 0 3px rgba(70, 98, 210, 0.45); /* ========================================================= * 10. Motion diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png deleted file mode 100644 index cc51a3d2..00000000 Binary files a/frontend/src/assets/hero.png and /dev/null differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b674..00000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/frontend/src/pages/DesignSystem/sections/Colors.tsx b/frontend/src/pages/DesignSystem/sections/Colors.tsx index 43ac7be4..08e3284f 100644 --- a/frontend/src/pages/DesignSystem/sections/Colors.tsx +++ b/frontend/src/pages/DesignSystem/sections/Colors.tsx @@ -26,9 +26,9 @@ const SEMANTIC_ROWS = [ { token: 'fg-muted', alias: 'sage-500', hex: '#4a5a7e', usage: '보조 텍스트' }, { token: 'fg-subtle', alias: 'sage-400', hex: '#6e7f9f', usage: '흐린 텍스트' }, { token: 'fg-disabled', alias: 'sage-200', hex: '#c6cedd', usage: '비활성' }, - { token: 'primary', alias: '브랜드 블루(로고)', hex: '#3e63d8', usage: 'Primary 액션·링크·강조' }, - { token: 'primary-hover', alias: '—', hex: '#3050be', usage: 'hover' }, - { token: 'primary-pressed', alias: '—', hex: '#274099', usage: 'active' }, + { token: 'primary', alias: '브랜드 블루', hex: '#4662d2', usage: 'Primary 액션·링크·강조' }, + { token: 'primary-hover', alias: '—', hex: '#3b54bd', usage: 'hover' }, + { token: 'primary-pressed', alias: '—', hex: '#31479f', usage: 'active' }, ] const STATUS_COLORS = [ diff --git a/frontend/src/pages/Home/ui/HomePage.tsx b/frontend/src/pages/Home/ui/HomePage.tsx index 9f059179..8cd012eb 100644 --- a/frontend/src/pages/Home/ui/HomePage.tsx +++ b/frontend/src/pages/Home/ui/HomePage.tsx @@ -2,8 +2,9 @@ import { useEffect } from 'react' import { useLocation } from 'react-router-dom' import { SiteNav } from '@/widgets/site-nav' import { HomeHero } from '@/widgets/home-hero' +import { HomeSteps } from '@/widgets/home-steps' +import { HomeFeatures } from '@/widgets/home-features' 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' @@ -27,12 +28,13 @@ export default function HomePage() { }, [hash]) return ( -
+
+ + -
diff --git a/frontend/src/shared/hooks/index.ts b/frontend/src/shared/hooks/index.ts index 02a5dea3..57706383 100644 --- a/frontend/src/shared/hooks/index.ts +++ b/frontend/src/shared/hooks/index.ts @@ -1,5 +1,3 @@ -export { useTypewriter } from './useTypewriter' -export type { UseTypewriterOptions } from './useTypewriter' export { useEventStream } from './useEventStream' export { useAnalysisProgress, analysisProgress } from './useAnalysisProgress' export type { AnalysisProgress } from './useAnalysisProgress' diff --git a/frontend/src/shared/hooks/useTypewriter.ts b/frontend/src/shared/hooks/useTypewriter.ts deleted file mode 100644 index d7f36398..00000000 --- a/frontend/src/shared/hooks/useTypewriter.ts +++ /dev/null @@ -1,61 +0,0 @@ -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 } -} diff --git a/frontend/src/shared/ui/Reveal/Reveal.tsx b/frontend/src/shared/ui/Reveal/Reveal.tsx new file mode 100644 index 00000000..22787e62 --- /dev/null +++ b/frontend/src/shared/ui/Reveal/Reveal.tsx @@ -0,0 +1,68 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react' + +export type RevealProps = { + children: ReactNode + /** 뷰포트 진입 후 지연(ms). 같은 블록 내 요소를 순차 등장시킬 때만 쓴다(최대 3~4단계). */ + delayMs?: number + className?: string + /** 감싸는 태그. 레이아웃(grid item 등)을 깨지 않게 필요 시 교체. */ + as?: 'div' | 'li' | 'section' +} + +/** + * 뷰포트에 들어올 때 한 번만 페이드업. + * + * 랜딩 섹션의 등장 모션 전용 — 스크롤을 따라 내용이 "도착하는" 정도의 절제된 모션만 + * 준다(이동 12px, 320ms). 한 번 보이면 다시 숨기지 않는다(스크롤 되돌릴 때 깜빡임 방지). + * + * `prefers-reduced-motion` 이면 관찰 없이 즉시 표시한다 — global.css 의 duration 무력화만으로는 + * 초기 opacity:0 이 남아 내용이 안 보일 수 있어서, 여기서 직접 분기한다. + */ +/** 관찰 없이 처음부터 보여줄 상황인지 — 모션 축소 선호 또는 IO 미지원. */ +function shouldSkipObserving() { + if (typeof IntersectionObserver === 'undefined') return true + if (typeof window === 'undefined') return false + return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true +} + +export function Reveal({ children, delayMs = 0, className = '', as = 'div' }: RevealProps) { + const ref = useRef(null) + // 초기값으로 결정한다 — effect 안에서 setState 하면 불필요한 연쇄 렌더가 생긴다. + const [shown, setShown] = useState(shouldSkipObserving) + + useEffect(() => { + if (shown) return + + const el = ref.current + if (!el) return + + const io = new IntersectionObserver( + (entries) => { + if (entries.some((e) => e.isIntersecting)) { + setShown(true) + io.disconnect() + } + }, + // 살짝 올라온 시점에 시작해 "이미 보이는데 뒤늦게 뜨는" 느낌을 없앤다. + { rootMargin: '0px 0px -12% 0px', threshold: 0.01 }, + ) + io.observe(el) + return () => io.disconnect() + }, [shown]) + + const Tag = as + + return ( + + {children} + + ) +} diff --git a/frontend/src/shared/ui/Reveal/index.ts b/frontend/src/shared/ui/Reveal/index.ts new file mode 100644 index 00000000..1bd709d8 --- /dev/null +++ b/frontend/src/shared/ui/Reveal/index.ts @@ -0,0 +1,2 @@ +export { Reveal } from './Reveal' +export type { RevealProps } from './Reveal' diff --git a/frontend/src/shared/ui/index.ts b/frontend/src/shared/ui/index.ts index 590d29f0..1c118836 100644 --- a/frontend/src/shared/ui/index.ts +++ b/frontend/src/shared/ui/index.ts @@ -9,3 +9,5 @@ export type { ToastTone, ToastItem } from './Toast' export { EmptyState } from './EmptyState' export type { EmptyStateProps } from './EmptyState' export { ListSkeleton } from './ListSkeleton' +export { Reveal } from './Reveal' +export type { RevealProps } from './Reveal' diff --git a/frontend/src/widgets/home-cta/ui/HomeCta.tsx b/frontend/src/widgets/home-cta/ui/HomeCta.tsx index 74f42026..476b21a6 100644 --- a/frontend/src/widgets/home-cta/ui/HomeCta.tsx +++ b/frontend/src/widgets/home-cta/ui/HomeCta.tsx @@ -1,54 +1,41 @@ import { Link } from 'react-router-dom' import { useGetStartedTarget } from '@/features/auth' +import { Reveal } from '@/shared/ui' export function HomeCta() { const getStartedTo = useGetStartedTarget() - return ( -
-
-
- -
-
+ return ( +
+
+ +

- Ready to level up? + 첫 면접, 지금 시작해보세요

-

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

+ GitHub 계정을 연결하면 자료 분석부터 바로 시작됩니다.

- Get Started - - → - + GitHub으로 시작하기
-
+
) diff --git a/frontend/src/widgets/home-faq/ui/HomeFaq.tsx b/frontend/src/widgets/home-faq/ui/HomeFaq.tsx index 59428efb..1edd441a 100644 --- a/frontend/src/widgets/home-faq/ui/HomeFaq.tsx +++ b/frontend/src/widgets/home-faq/ui/HomeFaq.tsx @@ -1,3 +1,5 @@ +import { Reveal } from '@/shared/ui' + const faqs = [ { q: 'Stack Up은 무료 크레딧을 제공하나요?', @@ -9,74 +11,66 @@ const faqs = [ }, { q: 'GitHub 외 다른 로그인은 가능한가요?', - a: '개발자를 위한 서비스이고 레포 분석이 핵심 기능이라, MVP단계에선 GitHub OAuth만 제공합니다.', + a: '개발자를 위한 서비스이고 레포 분석이 핵심 기능이라, MVP 단계에선 GitHub OAuth만 제공합니다.', }, { q: '꼬리질문은 얼마나 빨리 받을 수 있나요?', - a: '평균 3초 이내 응답을 목표로 합니다. Flash 모델과 사전 구축 RAG 인덱스로 지연을 최소화해요.', + a: '평균 3초 이내 응답을 목표로 합니다. 저지연 모델과 사전 구축한 검색 인덱스로 지연을 최소화해요.', }, { q: '음성 분석은 어떻게 동작하나요?', - a: 'WebRTC로 마이크·웹캠 스트림을 받아 말 속도(WPM)·간투어를 분석합니다. 권한을 거부하면 텍스트 입력으로 진행할 수 있어요.', + a: '마이크 스트림을 받아 말 속도(어절/분)·무음 구간·간투어를 측정합니다. 권한을 거부하면 텍스트 입력으로 진행할 수 있어요.', }, ] export function HomeFaq() { return ( -
-
-

- FAQ -

- -

- Questions, answered. -

- -
- -
-
-

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

- +
+ {/* 헤딩은 다른 섹션과 같은 좌측 거터에 맞추고, 목록만 읽기 좋은 폭으로 제한. */} + + 자주 묻는 질문 + + -
    - {faqs.map((f) => ( -
  • -
    - - - {f.q} - - + {faqs.map((f, i) => ( + +
    +
    + + + {f.q} + + + + + + +

    - + - -

    -
    - {f.a} -
    -
    -
  • + {f.a} +

    + +
+ ))}
diff --git a/frontend/src/widgets/home-features/index.ts b/frontend/src/widgets/home-features/index.ts new file mode 100644 index 00000000..529f59fc --- /dev/null +++ b/frontend/src/widgets/home-features/index.ts @@ -0,0 +1 @@ +export { HomeFeatures } from './ui/HomeFeatures' diff --git a/frontend/src/widgets/home-features/ui/FeatureVisuals.tsx b/frontend/src/widgets/home-features/ui/FeatureVisuals.tsx new file mode 100644 index 00000000..f452d95a --- /dev/null +++ b/frontend/src/widgets/home-features/ui/FeatureVisuals.tsx @@ -0,0 +1,163 @@ +/** + * 기능 섹션의 제품 UI 조각들. 값은 시연용 고정값이지만 축 이름·등급·지표는 실제 스키마와 같다. + * (채점 4축, 면접관 패널 라벨, 전달력 지표 = WPM·무음·간투어) + */ + +function Card({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} + +function AxisRow({ + label, + value, + max = 5, + weakest = false, +}: { + label: string + value: number + max?: number + weakest?: boolean +}) { + return ( +
  • + {label} + + + + + {value.toFixed(1)} + + {weakest ? ( + + 최저 + + ) : ( + + )} +
  • + ) +} + +/** 가장 낮은 축을 겨냥해 꼬리질문이 만들어지는 흐름. */ +export function FollowupVisual() { + return ( + +
    직전 답변 채점
    +
      + + + +
    + +
    + + 구체성을 겨냥한 꼬리질문 + +

    + “숫자로 말씀해 주세요. 개선 전후 p99 응답시간이 어떻게 바뀌었나요?” +

    +
    +
    + ) +} + +/** 점수에 근거와 인용이 붙는 화면. */ +export function ScoringVisual() { + return ( + +
    + 기술 정확도·깊이 + 79 +
    + +
    +
    점수 근거
    +

    + Q3에서 동시성 제어를 RDB 락으로만 설명해 분산 환경 고려가 빠졌습니다. +

    +
    + +
      + {['이력서 > 결제 시스템 개선', 'pay-api > OrderService.java'].map((s) => ( +
    • + + {s} +
    • + ))} +
    +
    + ) +} + +export function DeliveryVisual() { + const metrics = [ + { label: '말 속도', value: '142', unit: '어절/분' }, + { label: '무음', value: '4.2', unit: '초' }, + { label: '간투어', value: '8', unit: '회' }, + ] + return ( +
    +
    + {metrics.map((m) => ( +
    +
    {m.label}
    +
    + {m.value} + {m.unit} +
    +
    + ))} +
    +
    + ) +} + +export function PanelVisual() { + const rows = [ + { label: '백엔드', v: 79 }, + { label: '논리', v: 85 }, + { label: '전달', v: 88 }, + ] + return ( +
    +
      + {rows.map((r) => ( +
    • + {r.label} 면접관 + {r.v} +
    • + ))} +
    +
    + 종합 + 82 +
    +
    + ) +} + +export function ReportVisual() { + return ( +
    +
    + stack-up.shop/share/a1b2c3 + 복사 +
    +
    + + PDF 저장 + + + 링크 공유 + +
    +
    + ) +} diff --git a/frontend/src/widgets/home-features/ui/HomeFeatures.tsx b/frontend/src/widgets/home-features/ui/HomeFeatures.tsx new file mode 100644 index 00000000..d210efe8 --- /dev/null +++ b/frontend/src/widgets/home-features/ui/HomeFeatures.tsx @@ -0,0 +1,98 @@ +import { Reveal } from '@/shared/ui' +import { + DeliveryVisual, + FollowupVisual, + PanelVisual, + ReportVisual, + ScoringVisual, +} from './FeatureVisuals' + +const headingStyle = { + fontSize: 'clamp(26px, 3vw, 38px)', + lineHeight: 1.28, + letterSpacing: '-0.03em', + wordBreak: 'keep-all', +} as const + +const rows = [ + { + eyebrow: '꼬리질문', + title: '답을 흐리면\n그 지점을 다시 묻습니다', + desc: '답변을 구체성·논리·구조·정확성으로 채점하고, 가장 낮게 나온 축을 겨냥해 다음 질문을 만듭니다. 실제 면접에서 파고드는 방식과 같습니다.', + visual: , + }, + { + eyebrow: '근거 있는 채점', + title: '왜 그 점수인지\n자료로 확인합니다', + desc: '점수마다 근거 문장이 붙고, 판단의 출처가 된 이력서·레포의 대목을 그대로 보여줍니다. 근거를 못 찾으면 추측하지 않고 판단을 보류합니다.', + visual: , + }, +] + +const cards = [ + { + title: '전달력까지 함께', + desc: '말 속도와 침묵, 간투어를 측정해 내용과 별개로 전달력을 코칭합니다.', + visual: , + }, + { + title: '면접관은 여러 명', + desc: '직군별 면접관이 각자 기준으로 채점하고, 질문 비중에 따라 종합 점수를 냅니다.', + visual: , + }, + { + title: '리포트는 공유 가능', + desc: '결과를 링크로 공유하거나 PDF로 저장해 스터디·멘토링에 그대로 씁니다.', + visual: , + }, +] + +export function HomeFeatures() { + return ( +
    +
    + {rows.map((r, i) => ( +
    0 ? 'mt-24 lg:mt-32' : '' + }`} + > + + {r.eyebrow} +

    + {r.title} +

    +

    + {r.desc} +

    +
    + + {r.visual} + +
    + ))} + +
      + {cards.map((c, i) => ( + +
      +

      {c.title}

      +

      + {c.desc} +

      +
      {c.visual}
      +
      +
      + ))} +
    +
    +
    + ) +} diff --git a/frontend/src/widgets/home-hero/ui/HeroPreview.tsx b/frontend/src/widgets/home-hero/ui/HeroPreview.tsx new file mode 100644 index 00000000..45cb3e38 --- /dev/null +++ b/frontend/src/widgets/home-hero/ui/HeroPreview.tsx @@ -0,0 +1,81 @@ +/** + * 히어로의 제품 미리보기 — 실제 면접 화면의 한 장면을 그대로 재현한다. + * + * 스톡 사진 대신 제품 UI 를 보여주는 이유: 랜딩에서 증명해야 하는 건 "분위기"가 아니라 + * "답변을 파고드는 꼬리질문 + 근거 있는 4축 채점"이라는 동작이다. + * 데이터는 시연용 고정값(제품 문구·채점 축은 실제 스키마와 동일). + */ + +const SCORES = [ + { label: '구체성', value: '3.5' }, + { label: '논리', value: '4.0' }, + { label: '구조', value: '부분 STAR' }, + { label: '정확성', value: '4.5' }, +] + +export function HeroPreview() { + return ( +
    +
    +
    + + + + 면접 진행 중 + · 4번째 질문 +
    + + 프로젝트 심층 + +
    + +
    +
    +
    면접관
    +
    +

    + 결제 처리에 outbox 패턴을 도입했다고 쓰셨는데, 메시지 발행과 DB 커밋의 원자성은 + 어떻게 보장했나요? +

    +

    근거 · 이력서 > 결제 시스템 개선

    +
    +
    + +
    +
    내 답변
    +
    +

    + 트랜잭션 안에서 outbox 테이블에 같이 저장하고, 별도 워커가 폴링해서 발행했습니다. +

    +
    +
    + +
    +
    + 면접관 + + 꼬리질문 + +
    +
    +

    + 폴링 주기 사이에 중복 발행이 생길 수 있는데, 컨슈머 쪽 멱등성은 어떻게 처리하셨나요? +

    +
    +
    + +
    +
    직전 답변 채점
    +
    + {SCORES.map((s) => ( +
    +
    {s.label}
    +
    {s.value}
    +
    + ))} +
    +
    +
    +
    + ) +} diff --git a/frontend/src/widgets/home-hero/ui/HomeHero.tsx b/frontend/src/widgets/home-hero/ui/HomeHero.tsx index 5f922edf..97909f34 100644 --- a/frontend/src/widgets/home-hero/ui/HomeHero.tsx +++ b/frontend/src/widgets/home-hero/ui/HomeHero.tsx @@ -1,37 +1,83 @@ -import { Laptop } from './Laptop' -import { ScreenContent } from './ScreenContent' +import { Link } from 'react-router-dom' +import { useGetStartedTarget } from '@/features/auth' +import { Reveal } from '@/shared/ui' +import { HeroPreview } from './HeroPreview' + +const SOURCES = ['GitHub 레포', '이력서', '자소서'] export function HomeHero() { + const getStartedTo = useGetStartedTarget() + return ( -
    -
    - -
    - - - - -

    - - - Now Live · AI Based Interview - -

    +
    +
    +
    + +
      + {SOURCES.map((s) => ( +
    • + {s} +
    • + ))} +
    +
    + + +

    + 내 이력서를 아는 +
    + 면접관과 연습하세요 +

    +
    + + +

    + 올려둔 자료에서 질문을 만들고, 답변이 얕은 지점을 꼬리질문으로 파고듭니다. 끝나면 + 근거가 붙은 리포트를 받습니다. +

    +
    + + +
    + + GitHub으로 시작하기 + + + 어떻게 동작하나요? + +
    +
    + + +

    + 계정 연결만 하면 바로 시작 · 월 2회 무료 +

    +
    +
    + + + +
    ) diff --git a/frontend/src/widgets/home-hero/ui/Laptop.tsx b/frontend/src/widgets/home-hero/ui/Laptop.tsx deleted file mode 100644 index a0c505a0..00000000 --- a/frontend/src/widgets/home-hero/ui/Laptop.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import type { ReactNode } from 'react' - -export function Laptop({ children }: { children: ReactNode }) { - return ( -
    - {/* Lid */} -
    - {/* Bezel */} -
    - {/* Camera dot */} -
    - - {/* Screen */} -
    - {children} -
    -
    -
    - - {/* Base / hinge */} -
    -
    -
    -
    -
    - ) -} diff --git a/frontend/src/widgets/home-hero/ui/ScreenContent.tsx b/frontend/src/widgets/home-hero/ui/ScreenContent.tsx deleted file mode 100644 index b2d37f94..00000000 --- a/frontend/src/widgets/home-hero/ui/ScreenContent.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Link } from 'react-router-dom' -import { useGetStartedTarget } from '@/features/auth' -import { useTypewriter } from '@/shared/hooks' - -const TYPED_TEXT = 'Stack Up' - -export function ScreenContent() { - const { typed, done } = useTypewriter(TYPED_TEXT, { - startDelayMs: 850, - stepMs: 130, - }) - const getStartedTo = useGetStartedTarget() - - return ( -
    -

    - IT Interview Solution -

    - -

    - {typed} - -

    - -
    - - Get Started - - → - - -
    -
    - ) -} diff --git a/frontend/src/widgets/home-quote/index.ts b/frontend/src/widgets/home-quote/index.ts deleted file mode 100644 index e685ef69..00000000 --- a/frontend/src/widgets/home-quote/index.ts +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 842a2410..00000000 --- a/frontend/src/widgets/home-quote/ui/HomeQuote.tsx +++ /dev/null @@ -1,77 +0,0 @@ -export function HomeQuote() { - return ( -
    -
    -
    -
    - {/* decorative left panel */} -
    -
    -
    -
    -
    - Stack Up -
    -
    -
    - - {/* quote panel */} -
    -
    - “ -
    - -
    -

    - Stack Up은 직군을 가리지 않는 올인원 모의 면접 솔루션입니다. 이력서·자소서· - GitHub 레포에서 출발해, 답변의 깊이까지 따라 들어갑니다. -

    -
    - -
    -
    - 전 직군 · 맞춤 질문 · 음성·비언어 분석까지 -
    -
    -
    -
    -
    -
    -
    - ) -} diff --git a/frontend/src/widgets/home-services/ui/HomeServices.tsx b/frontend/src/widgets/home-services/ui/HomeServices.tsx index fb46d7e0..a0e08b2d 100644 --- a/frontend/src/widgets/home-services/ui/HomeServices.tsx +++ b/frontend/src/widgets/home-services/ui/HomeServices.tsx @@ -1,31 +1,34 @@ import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' -import { Modal } from '@/shared/ui' +import { Modal, Reveal } from '@/shared/ui' type ServiceCard = { - tag: string + plan: 'Pro' | 'Free' title: string - image: string + desc: string + tags: string[] } & ({ to: string } | { modal: 'role' }) const services: ServiceCard[] = [ { - tag: 'Pro Plan', - title: '이력서 심층 면접\nExperience Interview', - image: '/second-section-frontend-interview.png', + plan: 'Pro', + title: '이력서 심층 면접', + desc: '올려둔 이력서·자소서·GitHub 레포에서 질문을 만들고, 답변을 꼬리질문으로 파고듭니다.', + tags: ['이력서', '자소서', 'GitHub'], to: '/sessions/new', }, { - tag: 'Free Plan', - title: '직무 기술 면접\nRole-based Interview', - image: '/second-section-backend-interview.avif', - // 직무는 프론트/백엔드를 모달에서 고른 뒤 해당 연습으로 이동. + plan: 'Free', + title: '직무 기술 면접', + desc: '프론트엔드·백엔드에서 자주 나오는 기술 질문을 골라 가볍게 연습합니다.', + tags: ['프론트엔드', '백엔드'], modal: 'role', }, { - tag: 'Free Plan', - title: 'CS 전공 지식 면접\nCS Core Interview', - image: '/second-section-cs-interview.webp', + plan: 'Free', + title: 'CS 전공 지식 면접', + desc: '운영체제·네트워크·데이터베이스 같은 전공 필수 개념을 점검합니다.', + tags: ['OS', '네트워크', 'DB'], to: '/practice/cs', }, ] @@ -36,42 +39,47 @@ const ROLE_OPTIONS = [ ] as const const cardClass = - 'group relative block rounded-2xl overflow-hidden aspect-[3/4] bg-sage-800 text-left' + 'group flex h-full w-full flex-col rounded-2xl border border-border bg-surface-raised p-6 text-left transition-colors duration-fast hover:border-border-strong lg:p-7' function CardInner({ card }: { card: ServiceCard }) { return ( <> - {card.title} -
    - - - {card.tag} - - -
    -

    - {card.title} -

    +
    + + {card.plan} +
    + +

    {card.title}

    +

    + {card.desc} +

    + +
      + {card.tags.map((t) => ( +
    • + {t} +
    • + ))} +
    ) } @@ -86,33 +94,40 @@ export function HomeServices() { } return ( -
    -
    -

    - Our Services -

    +
    +
    + +

    + 지금 필요한 면접을 고르세요 +

    +

    + 이력서 기반 심층 면접부터 전공 개념 점검까지, 준비 단계에 맞게 고를 수 있습니다. +

    +
    -
    - {services.map((s) => - 'to' in s ? ( - - - - ) : ( - - ), - )} -
    +
      + {services.map((s, i) => ( + + {'to' in s ? ( + + + + ) : ( + + )} + + ))} +
    setRoleOpen(false)} title="직무 기술 면접"> diff --git a/frontend/src/widgets/home-steps/index.ts b/frontend/src/widgets/home-steps/index.ts new file mode 100644 index 00000000..e877939e --- /dev/null +++ b/frontend/src/widgets/home-steps/index.ts @@ -0,0 +1 @@ +export { HomeSteps } from './ui/HomeSteps' diff --git a/frontend/src/widgets/home-steps/ui/HomeSteps.tsx b/frontend/src/widgets/home-steps/ui/HomeSteps.tsx new file mode 100644 index 00000000..f3ff9de8 --- /dev/null +++ b/frontend/src/widgets/home-steps/ui/HomeSteps.tsx @@ -0,0 +1,137 @@ +import type { ReactNode } from 'react' +import { Reveal } from '@/shared/ui' + +function Check() { + return ( + + + + ) +} + +/** 카드 안에서 결과물을 실제 UI 조각으로 보여주는 미니 패널. */ +function Panel({ children }: { children: ReactNode }) { + return ( +
    {children}
    + ) +} + +const steps = [ + { + no: '01', + title: '자료 연결', + desc: 'GitHub 레포와 이력서·자소서를 올리면 알아서 읽고 정리합니다.', + panel: ( + +
      + {['이력서.pdf', 'github.com/me/pay-api', '자소서 3문항'].map((t) => ( +
    • + + {t} + 분석 완료 +
    • + ))} +
    +
    + ), + }, + { + no: '02', + title: '맞춤 면접', + desc: '내 경험에서 나온 질문에 답하고, 얕은 지점은 꼬리질문으로 다시 묻습니다.', + panel: ( + +
    + + 기술 선택 + + + 꼬리질문 + +
    +

    + “왜 Kafka 대신 RabbitMQ 였나요?” +

    +
    + ), + }, + { + no: '03', + title: '리포트', + desc: '점수와 함께 어디서 왜 깎였는지, 자료의 어느 대목이 근거인지 남습니다.', + panel: ( + +
      + {[ + { label: '종합', v: 82 }, + { label: '기술', v: 79 }, + { label: '전달력', v: 88 }, + ].map((r) => ( +
    • + {r.label} + + + + + {r.v} + +
    • + ))} +
    +
    + ), + }, +] + +export function HomeSteps() { + return ( +
    +
    + +

    + 면접 준비는 3단계로 끝납니다 +

    +

    + 질문을 직접 고르거나 답을 미리 외울 필요가 없습니다. +

    +
    + +
      + {steps.map((s, i) => ( + +
      + {s.no} +

      {s.title}

      +

      + {s.desc} +

      +
      {s.panel}
      +
      +
      + ))} +
    +
    +
    + ) +} diff --git a/frontend/src/widgets/site-footer/ui/SiteFooter.tsx b/frontend/src/widgets/site-footer/ui/SiteFooter.tsx index e8204f81..e1d42f98 100644 --- a/frontend/src/widgets/site-footer/ui/SiteFooter.tsx +++ b/frontend/src/widgets/site-footer/ui/SiteFooter.tsx @@ -1,5 +1,4 @@ import { Link } from 'react-router-dom' -import { useGetStartedTarget } from '@/features/auth' // 단순 뷰 섹션 widgets 에선 굳이 나누지 않는게 좋다고 판단했습니다. // 상수, 메세지 등 마찬가지 @@ -8,83 +7,56 @@ type FooterLink = { label: string; to?: string; href?: string } const columns: { title: string; links: FooterLink[] }[] = [ { - title: 'Company', + title: '서비스', links: [ - { label: 'Home', to: '/#top' }, - { label: 'About', to: '/#quote' }, - { label: 'FAQ', to: '/#faq' }, + { label: '이력서 심층 면접', to: '/sessions/new' }, + { label: '직무 기술 면접', to: '/practice/role' }, + { label: 'CS 전공 면접', to: '/practice/cs' }, ], }, { - title: 'Services', + title: '둘러보기', links: [ - { label: '이력서 심층 면접', to: '/sessions/new' }, - { label: '직무 기술 면접', to: '/practice/role' }, - { label: 'CS 전공 면접', to: '/practice/cs' }, + { label: '기능', to: '/#features' }, + { label: '동작 방식', to: '/#how' }, + { label: 'FAQ', to: '/#faq' }, ], }, { - title: 'Resources', + title: '리소스', links: [ - { label: 'Workspace', to: '/workspace' }, - { label: 'Design System', to: '/design-system' }, + { label: '워크스페이스', to: '/workspace' }, + { label: '디자인 시스템', to: '/design-system' }, { label: 'GitHub', href: 'https://github.com/Team-StackUp/stackup' }, ], }, ] export function SiteFooter() { - const getStartedTo = useGetStartedTarget() return ( -