diff --git a/CLAUDE.md b/CLAUDE.md index 52222823..24b597a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,6 +64,7 @@ PostgreSQL+pgvector · S3/MinIO | SSE 이벤트 스펙 | [`docs/event-stream.md`](./docs/event-stream.md) | | **디자인 시스템** | [`docs/design-system.md`](./docs/design-system.md) | | UI 패턴 (4-state, 폼, 토스트, 키보드) | [`docs/ui-patterns.md`](./docs/ui-patterns.md) | +| **프론트엔드 타입 시스템** | [`docs/frontend-types.md`](./docs/frontend-types.md) | | 보안 (인증, 암호화, 개인정보) | [`docs/security.md`](./docs/security.md) | | 옵저버빌리티 (trace, 로그, AI 비용) | [`docs/observability.md`](./docs/observability.md) | | 환경 변수 카탈로그 | [`docs/environment.md`](./docs/environment.md) | diff --git a/docs/README.md b/docs/README.md index d390a4ac..063c60e1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -32,6 +32,7 @@ ### 디자인·프론트엔드 - [`design-system.md`](./design-system.md) — 토큰, 컬러, 타이포그래피, 컴포넌트 인벤토리 - [`ui-patterns.md`](./ui-patterns.md) — 반복되는 UX 패턴, 상태 처리 (loading/empty/error) +- [`frontend-types.md`](./frontend-types.md) — TypeScript 타입 네이밍·레이어별 흐름·정의 위치 ### 보안·운영 - [`security.md`](./security.md) — 인증·인가, 토큰 암호화, 개인정보 처리 diff --git a/docs/coding-conventions.md b/docs/coding-conventions.md index 74009e8c..7fccfffd 100644 --- a/docs/coding-conventions.md +++ b/docs/coding-conventions.md @@ -21,6 +21,16 @@ 허용: `id`, `url`, `uri`, `api`, `db`, `pg`, `mq`, `ai`, `llm`, `rag`, `stt`, `tts`, `pdf` 비권장: `usr`, `msg`, `cfg`, `tmp` → `user`, `message`, `config`, `temp` 사용 +### 1.4 타입 (TypeScript) + +프론트엔드 TypeScript 타입 규약은 분리 문서로 관리한다 → [**`frontend-types.md`**](./frontend-types.md). + +요점만: +- PascalCase + 의미 있는 접미사 (`Dto` / `Request` / `Response` / `Result` / `Model` / `Props` / `State` / `Options` / `Id`). +- `I` / `T` Hungarian prefix 금지. +- 타입은 사용되는 레이어에 산다. `Dto → Entity → Model → Props` 단방향. +- 전역 `types/` 폴더 금지. 슬라이스 내부에서 시작, 3곳 이상 중복 시 `shared` 승격. + --- ## 2. 함수·메서드 설계 diff --git a/docs/frontend-types.md b/docs/frontend-types.md new file mode 100644 index 00000000..8909b664 --- /dev/null +++ b/docs/frontend-types.md @@ -0,0 +1,124 @@ +# 프론트엔드 타입 시스템 — TypeScript + +> StackUp 프론트엔드의 TypeScript 타입 규약. **타입은 사용되는 레이어에 산다.** 이름의 접미사가 책임 경계를 선언한다. +> +> 상위 컨텍스트 — [coding-conventions.md](./coding-conventions.md) (언어 공통 규약), [`frontend/CLAUDE.md`](../frontend/CLAUDE.md) (FSD 구조) +> 적용 범위 — [`frontend/src/`](../frontend/src/) + +--- + +## 목차 + +1. [원칙](#1-원칙) +2. [접미사 ↔ 레이어 매트릭스](#2-접미사--레이어-매트릭스) +3. [레이어별 타입 흐름](#3-레이어별-타입-흐름) +4. [`type` vs `interface`](#4-type-vs-interface) +5. [정의 위치](#5-정의-위치) +6. [shared 승격 임계](#6-shared-승격-임계) +7. [안티패턴](#7-안티패턴) + +--- + +## 1. 원칙 + +- **PascalCase + 의미 있는 접미사**. 접미사가 타입의 책임·레이어를 신고한다. +- `I` / `T` Hungarian prefix 금지 — TypeScript 컴파일러가 `type` / `interface` 를 충분히 구분한다. +- **단방향 흐름**: `Dto → Entity → Model → Props`. 역방향 import 금지. +- **전역 `types/` 폴더 금지**. 슬라이스 내부에서 시작, 진짜 공유될 때만 끌어올린다. + +--- + +## 2. 접미사 ↔ 레이어 매트릭스 + +| 접미사 | 레이어 | 책임 | 예 | +|---|---|---|---| +| `XxxDto` | `shared/api/` (OpenAPI 자동 생성) | 백엔드 wire format 스냅샷. 수정 금지 | `SessionDto`, `UserDto` | +| `XxxRequest` / `XxxPayload` | API 호출부, 이벤트 publisher | HTTP 요청 body, RabbitMQ 메시지 payload | `CreateSessionRequest`, `ResumeAnalyzedPayload` | +| `XxxResponse` | API 호출부 | HTTP 응답 raw shape (수동 정의 시) — **함수 반환에 사용 ✗** | `LoginResponse` | +| `Xxx` (접미사 없음) | `domain/{slice}/model/` | 비즈니스 엔터티. Dto에서 매핑됨 | `User`, `Session`, `Resume` | +| `XxxResult` | 도메인 / 유즈케이스 함수 반환 | 비즈니스 로직 출력 (HTTP 비종속) | `AnalysisResult`, `ValidationResult` | +| `XxxModel` | `features/*/ui/`, `widgets/*/ui/` | UI 표시 전용 가공 데이터 | `SessionListItemModel`, `ServiceCardModel` | +| `XxxProps` | 컴포넌트 옆 | React props | `ButtonProps`, `HomeHeroProps` | +| `XxxState` | store / state machine | **명시적** 상태 모델. 단발 `useState` shape 에 사용 ✗ | `AuthState`, `SessionMachineState` | +| `XxxOptions` | 함수·훅 설정 | 옵셔널 인자 묶음 | `UseTypewriterOptions` | +| `XxxId` | 어디든 | branded identifier | `type SessionId = number & { readonly __brand: 'SessionId' }` | + +--- + +## 3. 레이어별 타입 흐름 + +``` + [Backend OpenAPI] + │ openapi-typescript 자동 생성 + ▼ + shared/api/generated.ts ── XxxDto + │ 매퍼 함수 (toUser, toSession) + ▼ + domain/{slice}/model/ ── Xxx (Entity) + │ feature / widget 에서 UI 가공 + ▼ + features|widgets/*/ui/ ── XxxModel + │ props 로 주입 + ▼ + React Component ── XxxProps +``` + +**경계 규칙**: + +- `XxxDto` 는 `shared/api/` 밖으로 새지 않는다. UI · 도메인이 Dto 를 직접 import 하면 anti-corruption layer 가 무너진다. +- `Xxx` (Entity) 는 `domain/` 이 소유. features / widgets 는 read-only 로 참조한다. +- `XxxModel` 은 슬라이스 내부에 머문다. 다른 슬라이스로 export 하지 않는다 — UI 표시 형태가 누수되면 변경 비용이 폭증한다. + +--- + +## 4. `type` vs `interface` + +`type` 기본값. `interface` 는 두 경우에만: + +1. 클래스가 `implements` 할 때 (`ErrorBoundary` 등) +2. declaration merging 필요 시 (외부 라이브러리 augmentation) + +```ts +// 좋음 +type SessionStatus = 'READY' | 'IN_PROGRESS' | 'COMPLETED'; +type CreateSessionRequest = { userId: UserId; mode: SessionMode }; +type AnalysisResult = { score: number; suggestions: string[] }; + +// 좋음 — implements 가 필요한 케이스 +interface ErrorBoundaryProps { children: ReactNode; fallback: ReactNode } +class ErrorBoundary extends Component { ... } +``` + +--- + +## 5. 정의 위치 + +| 무엇 | 어디 | +|---|---| +| 컴포넌트 props (단일 사용) | `{Component}.tsx` 상단, **export 안 함** | +| 외부에서도 import 되는 props | `{Component}.types.ts` | +| 도메인 엔터티 | `domain/{slice}/model/types.ts` | +| 슬라이스 내 `XxxModel` | `features/{slice}/model/types.ts` 또는 `ui/` 내부 | +| API `XxxDto` | `shared/api/generated.ts` (OpenAPI 재생성으로만 갱신) | + +**전역 `src/types/` 폴더는 만들지 않는다.** 슬라이스 내부에서 시작하고, 진짜 공유될 때만 끌어올린다. + +--- + +## 6. shared 승격 임계 + +타입을 `shared/` 로 올리는 시점은 **서로 다른 슬라이스 3곳 이상에서 동일한 shape 이 반복될 때**. 그 전엔 슬라이스 안에 산다. + +> "언젠가 쓸지도 모르니 미리 shared 로" 는 항상 잘못된 추상화로 끝난다. + +--- + +## 7. 안티패턴 + +- ❌ `XxxResponse` 를 함수 반환 타입으로 사용 — `XxxResult` 로. +- ❌ UI 컴포넌트가 `SessionDto` 를 props 로 받음 — `SessionListItemModel` 로 매핑. +- ❌ 로컬 `useState<{ ... }>` shape 에 `XxxState` 이름 붙이고 export. +- ❌ `export type Props = { ... }` 같은 generic export — `{Component}Props` 로. +- ❌ `src/types/` 또는 `src/shared/types/` 전역 폴더 (junk drawer 화). +- ❌ `IXxx`, `TXxx` Hungarian prefix. +- ❌ `Dto` 가 `features/` / `widgets/` / `domain/` 에서 import 되는 경우 — boundary 위반. diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg deleted file mode 100644 index 6893eb13..00000000 --- a/frontend/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg deleted file mode 100644 index e9522193..00000000 --- a/frontend/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/last-section-get-started.png b/frontend/public/last-section-get-started.png new file mode 100644 index 00000000..44728d4f Binary files /dev/null and b/frontend/public/last-section-get-started.png differ diff --git a/frontend/public/second-section-backend-interview.avif b/frontend/public/second-section-backend-interview.avif new file mode 100644 index 00000000..aa31614f Binary files /dev/null and b/frontend/public/second-section-backend-interview.avif differ diff --git a/frontend/public/second-section-cs-interview.avif b/frontend/public/second-section-cs-interview.avif new file mode 100644 index 00000000..2b5c1c86 Binary files /dev/null and b/frontend/public/second-section-cs-interview.avif differ diff --git a/frontend/public/second-section-frontend-interview.png b/frontend/public/second-section-frontend-interview.png new file mode 100644 index 00000000..0772b77a Binary files /dev/null and b/frontend/public/second-section-frontend-interview.png differ 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/CLAUDE.md b/frontend/src/widgets/CLAUDE.md new file mode 100644 index 00000000..8885e7b5 --- /dev/null +++ b/frontend/src/widgets/CLAUDE.md @@ -0,0 +1,141 @@ +# `widgets/` — 위젯 슬라이스 레이어 + +> 페이지를 구성하는 "큰 UI 블록" 단위. 한 widget = 자체적으로 의미를 가지는 섹션·영역. + +상위: [`../../CLAUDE.md`](../../CLAUDE.md) + +--- + +## 1. widget이 되어야 하는 것 / 아닌 것 + +**widget O**: +- "사이트 네비게이션" — `widgets/site-nav` +- "사이트 푸터" — `widgets/site-footer` +- "홈 히어로 섹션" — `widgets/home-hero` +- "서비스 카드 그리드" — `widgets/home-services` +- "FAQ 아코디언 섹션" — `widgets/home-faq` +- "워크스페이스 사이드바" (예정) — `widgets/workspace-sidebar` +- "면접 진행 컨트롤 바" (예정) — `widgets/interview-control` + +**widget X**: +- "Button 자체" — `shared/ui/Button` +- "이력서를 업로드한다" — `features/resume` (사용자 액션 흐름) +- "Session 도메인 모델" — `domain/session` + +**판단 기준**: +- 페이지에 박혔을 때 **자체적으로 의미를 가지는 UI 블록**이면 widget. +- 작은 범용 컴포넌트라면 `shared/ui`. +- 사용자 액션(클릭 → API 호출 → 상태 변경)이 본질이라면 `features/`. + +--- + +## 2. 슬라이스 구조 + +``` +widgets/{name}/ +├── ui/ # 컴포넌트 (HomeHero, SiteNav, ...) +│ └── {Widget}.tsx +├── model/ # widget 단위 훅·상태 (있다면) +├── lib/ # widget 내부 유틸 (있다면) +└── index.ts # public API +``` + +**대부분의 widget은 `ui/` + `index.ts` 두 개로 끝난다.** `model/`·`lib/`는 필요해질 때만 추가. + +### Public API (index.ts) +메인 컴포넌트만 export. 내부 sub-component는 export 하지 않는다. +```ts +// widgets/home-hero/index.ts +export { HomeHero } from './ui/HomeHero' + +// ui/Laptop.tsx, ui/ScreenContent.tsx 는 내부 구현 — re-export 안 함 +``` + +슬라이스 폴더는 **kebab-case**, 컴포넌트 export 는 **PascalCase**. + +--- + +## 3. 의존성 규칙 + +``` +widgets/{X} → features/*, domain/*, shared/* ✓ +widgets/{X} → widgets/{Y} ✗ (다른 widget import 금지) +widgets/{X} → pages/*, app/* ✗ +``` + +위젯끼리 import가 필요하면: +- 공통 UI 조각을 `shared/ui` 로 추출 +- 또는 페이지에서 composition 으로 연결 (페이지가 widget 조립 책임) + +--- + +## 4. 명명 규칙 + +| Prefix | 범위 | 예 | +|---|---|---| +| `site-*` | 사이트 전역 (여러 페이지 재사용) | `site-nav`, `site-footer` | +| `home-*` | 홈페이지 전용 | `home-hero`, `home-services`, `home-faq` | +| `workspace-*` (예정) | 워크스페이스 페이지 전용 | `workspace-sidebar` | +| `interview-*` (예정) | 면접 페이지 전용 | `interview-control`, `interview-transcript` | + +> prefix 가 페이지 종속을 명시적으로 표현. 한 페이지에서만 쓰이는 widget 도 정상. + +--- + +## 5. widget 인벤토리 + +| Widget | 책임 | 사용 페이지 | +|---|---|---| +| `site-nav` | 상단 네비게이션, 스크롤 시 배경 전환 | 전역 | +| `site-footer` | 푸터 (브랜드, 링크 컬럼, 카피라이트) | 전역 | +| `home-hero` | 노트북 목업 + 타이핑 인트로 (`useTypewriter`) | `/` | +| `home-services` | 서비스 카드 3개 그리드 | `/` | +| `home-quote` | 다크 그린 quote · 팀 크레딧 | `/` | +| `home-faq` | FAQ 아코디언 (`
` 기반) | `/` | +| `home-cta` | 풀-블리드 CTA 배너 | `/` | + +--- + +## 6. widget vs feature 비교 + +| 질문 | widget | feature | +|---|---|---| +| 본질 | 페이지 영역을 채우는 **UI 구성** | 사용자가 수행하는 **액션 흐름** | +| 서버 호출 | 보통 없음 (data 는 props 로 받음) | 있음 (mutation / query) | +| User Story 매핑 | 없음 (UI 표현 단위) | US-XX 1개 | +| 상태 | UI 로컬 상태 정도 | 도메인 상태 / 서버 상태 | +| 예 | `home-hero`, `site-nav` | `auth`, `resume`, `interview` | + +같은 영역이라도 **데이터 표시만**이면 widget, **사용자 액션 흐름**이 있으면 feature. widget 이 feature 의 훅을 끌어와 페이지 한 구역에 박는 패턴은 정상 (예: `widgets/interview-control` 이 `features/interview` 의 mutation 훅 사용). + +--- + +## 7. 타입 사용 + +widget UI 컴포넌트가 받는 데이터는 [`/docs/frontend-types.md`](../../../docs/frontend-types.md) 의 단방향 흐름을 따른다. + +``` +domain/{slice}/model/ (Entity) → widget 내부에서 XxxModel 로 가공 → 컴포넌트 Props +``` + +`XxxDto` 를 widget 안에서 직접 import 하면 boundary 위반. 매핑은 `features/*/api/` 또는 `domain/*/model/` 에서. + +--- + +## 8. 신규 widget 추가 절차 + +1. `widgets/{name}/ui/{Widget}.tsx` 생성 (PascalCase 파일·컴포넌트명) +2. `widgets/{name}/index.ts` 에 메인 컴포넌트만 export +3. 페이지에서 import 해서 조립 (`pages/Xxx/ui/XxxPage.tsx`) +4. 본 문서 §5 인벤토리 등록 + +--- + +## 9. 안티패턴 + +- ❌ widget 이 다른 widget 을 직접 import — 페이지에서 composition 으로 +- ❌ widget 이 라우팅 결정 (`useNavigate` 등) — 페이지 책임 +- ❌ widget 이 `XxxDto` 직접 사용 — [`/docs/frontend-types.md`](../../../docs/frontend-types.md) 단방향 흐름 위반 +- ❌ widget 내부 sub-component 를 `index.ts` 에서 re-export — 캡슐화 깨짐 +- ❌ "재사용성을 위한" widget 미리 만들기 — 한 페이지에서만 써도 widget 이 되는 게 정상. 재사용은 필요해지면. +- ❌ widget 에서 직접 서버 호출 — `features/*/api/` 또는 `domain/*` 을 경유 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 + + + + + +
+
+ ) +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index b2a52c6b..77a86963 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -25,17 +25,15 @@ "noUncheckedSideEffectImports": true, /* Path mapping */ - "baseUrl": ".", "paths": { - "@/*": ["src/*"], - "@/shared/*": ["src/shared/*"], - "@/entities/*": ["src/domain/*"], - "@/features/*": ["src/features/*"], - "@/pages/*": ["src/pages/*"], - "@/app/*": ["src/app/*"], - }, - - + "@/*": ["./src/*"], + "@/shared/*": ["./src/shared/*"], + "@/entities/*": ["./src/domain/*"], + "@/features/*": ["./src/features/*"], + "@/widgets/*": ["./src/widgets/*"], + "@/pages/*": ["./src/pages/*"], + "@/app/*": ["./src/app/*"] + } }, "include": ["src"]