# o2o-infinith-frontend

INFINITH 마케팅 분석 플랫폼의 프론트엔드. React 19 + Vite + Tailwind v4 + TanStack Query.

## 기술 스펙

| 영역 | 라이브러리 / 도구 | 비고 |
| --- | --- | --- |
| 언어 | TypeScript ~5.8 | |
| 빌드 | Vite 6 | `@vitejs/plugin-react` |
| UI 프레임워크 | React 19 | |
| 스타일 | Tailwind CSS v4 | CSS-first 방식 (config 파일 없음) |
| 컴포넌트 | [shadcn/ui](https://ui.shadcn.com/) | Radix + Tailwind 조합, `src/shared/ui/` 에 복사해 사용 |
| 아이콘 | lucide-react, 커스텀 (`src/shared/icons/`) | |
| 애니메이션 | motion (Framer Motion 후속), tw-animate-css | |
| 라우팅 | react-router 7 | |
| 데이터 페칭 | TanStack Query 5 | |
| HTTP 클라이언트 | ky | `customFetcher` 의 베이스 |
| API SDK 생성 | orval 7 | OpenAPI → React Query 훅 |
| 클라이언트 상태 | zustand 5 | |
| PDF 출력 | jspdf, html2canvas-pro | |
| 폰트 | Pretendard Variable (self-host), Playfair Display | |

## 시작하기

### 로컬 (Node)

```bash
npm install
npm run dev        # localhost:3000
npm run build      # 프로덕션 빌드
npm run lint       # tsc --noEmit
npm run api:gen    # SDK 재생성 (orval)
```

### Docker (개발)

[Dockerfile.dev](Dockerfile.dev) + [docker-compose.yml](docker-compose.yml) — Vite dev 서버를 컨테이너에서 돌리고 호스트 소스 변경을 HMR로 반영.

```bash
docker compose up           # 시작
docker compose up --build   # 의존성 바뀐 경우
docker compose down         # 종료
```

## 프로젝트 구조

```
src/
├── app/              # 라우터 / 프로바이더 / 진입점 셋업
├── config/           # 앱 전역 설정
├── features/         # 도메인별 모듈 (페이지 + 훅 + 컴포넌트 + 데이터)
│   ├── admin/        # 관리자 (API 대시보드 등)
│   ├── auth/         # 인증
│   ├── channels/     # 채널 검증 / enrichment
│   ├── clinics/      # 병원 프로필
│   ├── dev/          # 개발용 페이지
│   ├── distribution/ # 콘텐츠 배포
│   ├── landing/      # 랜딩
│   ├── performance/  # 성과 대시보드
│   ├── plan/         # 마케팅 플랜
│   ├── pricing/      # 요금제
│   ├── report/       # 분석 리포트 (로딩/뷰어)
│   └── studio/       # 콘텐츠 스튜디오
├── shared/
│   ├── api/          # orval-generated SDK + HTTP 어댑터
│   │   ├── api.ts        # customFetcher (ky 기반 mutator)
│   │   ├── generated/    # ⚠️ orval 자동생성 (직접 수정 금지)
│   │   └── model/        # ⚠️ orval 자동생성 타입
│   ├── constants/    # 전역 상수
│   ├── hooks/        # 범용 훅
│   ├── icons/        # 커스텀 아이콘
│   ├── layouts/      # 공통 레이아웃
│   ├── lib/          # 유틸 헬퍼
│   ├── types/        # 공통 타입
│   ├── ui/           # shadcn 기반 공용 컴포넌트
│   └── utils/        # 유틸 함수
├── styles/
│   └── index.css     # ★ Tailwind + 디자인 토큰 (브랜드 색 / 폰트 / radius)
└── assets/           # 정적 자산 (폰트, 이미지 등)
```

기능 모듈 내부 컨벤션:

```
features/<name>/
├── pages/        # 라우트에 매핑되는 페이지 컴포넌트
├── components/   # 해당 기능 전용 컴포넌트
├── hooks/        # 기능 전용 훅 (use<Feature>.ts)
├── lib/          # transform / parser 등 순수 로직
├── data/         # mock / 시드 상수
├── types/        # 기능 전용 타입
└── routes.tsx    # 라우트 정의
```

## 디자인 토큰

전부 [src/styles/index.css](src/styles/index.css)에서 관리. Tailwind v4의 CSS-first 방식이라 별도 `tailwind.config.js` 파일이 없다.

| 블록 | 위치 | 용도 |
| --- | --- | --- |
| `:root { --brand-* }` | 상단 | 브랜드 색 원본 (purple / navy / rose / earth …) |
| `:root { --status-* }` | 중간 | semantic 상태 색 (critical / warning / good / info) |
| `:root { --background, --primary, … }` | shadcn 영역 | shadcn 토큰 → 브랜드 색 매핑 |
| `.dark { … }` | 다크모드 | 다크 모드 오버라이드 |
| `@theme inline { --color-*, --font-* }` | 하단 | Tailwind 유틸리티로 노출 (`bg-brand-purple`, `text-status-critical-text` 등) |

새 색을 만들고 싶다면 `:root`에 `--brand-xxx` 추가 → `@theme inline`에 `--color-brand-xxx: var(--brand-xxx)` 추가 → `bg-brand-xxx` 유틸로 사용.

> **왜 `tailwind.config.js` 가 없나요?**
> Tailwind v4 부터 설정을 JS 파일이 아닌 CSS의 `@theme` 블록에서 한다. 폰트, 색, radius, breakpoint 같은 모든 토큰을 [src/styles/index.css](src/styles/index.css) 하나에서 관리한다.

## 커스텀 클래스 / CSS

| 위치 | 내용 |
| --- | --- |
| [src/styles/custom.css](src/styles/custom.css) | 커스텀 유틸 클래스(`.text-gradient`, `.glass-card`, `.gradient-bg`, `.soft-gradient`) + `@keyframes` + 애니메이션 유틸(`.animate-blob`, `.animation-delay-*`) |
| [src/styles/index.css](src/styles/index.css) `@layer base` | 전역 element 스타일 (`body`, `h1`~`h6` 등) |

새 유틸 클래스는 `custom.css` 에 추가. 컴포넌트 단위 스타일은 Tailwind 유틸리티로 JSX에서 직접 작성하는 게 기본.

## 폰트 변경

Google Fonts로 가져오는 방식. 두 곳만 수정하면 된다.

1. **[index.html](index.html)** — Google Fonts `<link>` 의 `family=` 파라미터에 원하는 폰트 추가/교체
   ```html
   <link href="https://fonts.googleapis.com/css2?family=원하는폰트:wght@400;700&display=swap" rel="stylesheet">
   ```
2. **[src/styles/index.css](src/styles/index.css)** 의 `@theme inline` 에서 `--font-sans` (본문) 또는 `--font-serif` (제목) 의 첫 항목을 새 폰트 이름으로 교체

## 백엔드 API / orval

OpenAPI 스펙을 가져와 SDK(React Query 훅 포함)를 자동생성한다.

- 입력: [orval.config.ts](orval.config.ts) 의 `input` 필드 (예: `http://localhost:8001/openapi.json`)
- 출력: `src/shared/api/generated/` (함수 + 훅) + `src/shared/api/model/` (타입)
- HTTP 어댑터: [src/shared/api/api.ts](src/shared/api/api.ts) — ky 기반 `customFetcher`. 4xx/5xx도 throw하지 않고 `{ status, data, headers }`로 반환.

### 사용 예

```ts
// 쿼리 훅
import { useGetReport } from '@/shared/api/generated/reports/reports'
const { data, isLoading } = useGetReport(id, { query: { enabled: !!id } })

// 뮤테이션 훅
import { useStartAnalysis } from '@/shared/api/generated/analyses/analyses'
const { mutateAsync } = useStartAnalysis()
await mutateAsync({ data: { url: '...' } })

// imperative 호출(폴링 등)
import { getAnalysisStatus } from '@/shared/api/generated/analyses/analyses'
const res = await getAnalysisStatus(runId)
```

### 재생성

백엔드 스펙이 바뀌면:

```bash
npm run api:gen
```

`clean: true`라 `generated/`, `model/` 폴더는 매번 비워지고 새로 생성된다. 직접 수정 금지.

## API 프록시 (개발)

[vite.config.ts](vite.config.ts) 에서 `/api/*` 요청을 백엔드로 프록시한다.

- 기본 타겟: `http://localhost:8001`
- 도커 안에서 dev 서버 돌 때: `http://host.docker.internal:8001` (CHOKIDAR_USEPOLLING=true 일 때 자동)
- 오버라이드: `VITE_API_TARGET=http://40.82.133.44:8001` 환경변수

## shadcn/ui

[ui.shadcn.com](https://ui.shadcn.com/) 컴포넌트는 패키지가 아니라 **소스 복사** 방식으로 사용한다. 추가/수정한 컴포넌트는 [src/shared/ui/](src/shared/ui/) 에 위치하며, Tailwind 토큰(`--background`, `--primary` 등)을 통해 디자인 시스템과 연결된다.

새 컴포넌트 추가:

```bash
npx shadcn@latest add button
```
