You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1046 lines
39 KiB
Markdown
1046 lines
39 KiB
Markdown
# Phase 0 — 프로젝트 스캐폴딩 & 개발 환경
|
|
|
|
> 한 줄 요약: 모노레포(`frontend/` + `backend/`)를 초기화하고 Next.js · FastAPI · SQLite · Ollama 4개 축을 연결한 뒤, `/api/health` · `/api/llm/health` · 프론트 dev 서버 부팅으로 "스모크 검증"까지 끝내는 단계.
|
|
>
|
|
> 이 문서는 `dev/` 문서 세트의 일부입니다 — 먼저 `overview.md`를 읽으세요. 이후 순서는 `phase-1-design-system.md` → `phase-2-backend.md` → (`phase-3-tasks.md` → `phase-4-inbox.md` → `phase-5-dashboard.md`) → `phase-6-integration.md` 입니다.
|
|
|
|
---
|
|
|
|
## 1. 개요 & 목표
|
|
|
|
Phase 0이 끝나면 다음이 "실제로 동작"한다.
|
|
|
|
- 레포 루트(`/Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace`) 아래에 `frontend/`(Next.js App Router + TS)와 `backend/`(FastAPI + SQLite)가 골격으로 존재한다.
|
|
- `pnpm dev`(또는 `npm run dev`)로 프론트가 `http://localhost:3000`에서 뜬다.
|
|
- `uvicorn app.main:app --reload`로 백엔드가 `http://localhost:8000`에서 뜬다.
|
|
- `GET http://localhost:8000/api/health` → `{"status":"ok"}` (200).
|
|
- `GET http://localhost:8000/api/llm/health` → Ollama가 켜져 있으면 `reachable:true`, 꺼져 있으면 `reachable:false`로 **에러 없이** 응답(서버가 죽지 않는다).
|
|
- 백엔드 CORS가 프론트 origin(`http://localhost:3000`)을 허용한다.
|
|
- SQLite 엔진/세션(`DATABASE_URL=sqlite:///./ari.db`)과 alembic 초기화가 준비된다(테이블 생성·시드는 `phase-2-backend.md`에서).
|
|
- 빈 테스트(pytest / vitest)가 통과하고, lint(ruff/eslint)가 깨끗하다.
|
|
- 루트 `Makefile`로 두 서버를 동시에 띄울 수 있다.
|
|
|
|
이 Phase는 **기능 0, 인프라 100**이다. UI 픽셀 재현(디자인 토큰 이식)은 `phase-1-design-system.md`, 데이터 모델·시드·전체 REST는 `phase-2-backend.md`에서 다룬다. 여기서는 "두 서버가 동시에 살아 있고 서로 + Ollama와 악수(handshake)한다"만 보장한다.
|
|
|
|
### 디자인 충실도의 출발점(왜 지금 토큰을 미리 본다)
|
|
|
|
Phase 0에서는 토큰 CSS를 작성하지 않지만, `frontend/styles/tokens.css` 자리표시자와 폰트 로딩만은 지금 잡아둔다. 원본 진입 HTML(`design-reference/대시보드.html`)이 로드하는 폰트가 곧 우리 앱 셸의 폰트이기 때문이다.
|
|
|
|
```html
|
|
<!-- design-reference/대시보드.html (lines 8~17) -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<link href="https://fonts.googleapis.com/css2?family=Onest:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css" />
|
|
```
|
|
|
|
→ 폰트는 `--font-disp: Onest`, `--font-ui: Pretendard`, `--font-mono: "DM Mono"`. 본문 기본은 Pretendard이고 한국어 가독성을 위해 `word-break: keep-all; letter-spacing: -0.011em`를 body에 건다(원본 `dash.css` 79~88행). Phase 0에서는 이 폰트 링크와 body 기본 규칙만 자리 잡고, 색/그림자/글래스 전체 토큰은 Phase 1에서 `:root`로 이식한다.
|
|
|
|
---
|
|
|
|
## 2. 선행 조건 & 산출물
|
|
|
|
### 2.1 선행 조건(로컬에 미리 설치)
|
|
|
|
| 도구 | 권장 버전 | 확인 명령 | 비고 |
|
|
|---|---|---|---|
|
|
| Node.js | 20 LTS 이상 | `node -v` | Next.js 14/15 App Router 요구사항 |
|
|
| pnpm | 9.x (또는 npm 10.x) | `pnpm -v` | 프론트 패키지 매니저. 없으면 `corepack enable` |
|
|
| Python | 3.11 이상 | `python3 --version` | FastAPI + SQLModel |
|
|
| uv | 최신(권장) | `uv --version` | 백엔드 패키지 매니저(없으면 pip+venv 폴백) |
|
|
| Ollama | 최신 | `ollama --version` | 로컬 LLM. **모델 비종속**(아래 5절) |
|
|
| git | 임의 | `git --version` | 레포 초기화 |
|
|
|
|
> 이 Phase는 Phase 의존성이 없다(로드맵의 첫 단계). `overview.md`만 읽고 진행 가능.
|
|
|
|
### 2.2 산출물(Deliverables)
|
|
|
|
```
|
|
/Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace
|
|
├─ frontend/
|
|
│ ├─ app/
|
|
│ │ ├─ layout.tsx 앱 셸 자리표시자(폰트/메타)
|
|
│ │ └─ page.tsx / → /dashboard 리다이렉트(자리표시자)
|
|
│ ├─ components/ (빈 디렉터리 + .gitkeep)
|
|
│ ├─ lib/ api.ts(베이스 URL 헬퍼), types.ts(빈 export)
|
|
│ ├─ styles/ tokens.css(자리표시자), globals.css
|
|
│ ├─ tests/ smoke.test.ts (vitest)
|
|
│ ├─ public/
|
|
│ ├─ .env.local NEXT_PUBLIC_API_BASE
|
|
│ ├─ next.config.ts
|
|
│ ├─ tsconfig.json
|
|
│ ├─ package.json
|
|
│ ├─ vitest.config.ts
|
|
│ ├─ .eslintrc / eslint.config.mjs
|
|
│ └─ .prettierrc
|
|
├─ backend/
|
|
│ ├─ app/
|
|
│ │ ├─ __init__.py
|
|
│ │ ├─ main.py FastAPI 인스턴스 + CORS + 라우터 등록
|
|
│ │ ├─ config.py 환경변수 settings
|
|
│ │ ├─ db.py 엔진/세션
|
|
│ │ ├─ routers/
|
|
│ │ │ ├─ __init__.py
|
|
│ │ │ ├─ health.py GET /api/health
|
|
│ │ │ └─ llm.py GET /api/llm/health
|
|
│ │ └─ llm/
|
|
│ │ ├─ __init__.py
|
|
│ │ └─ ollama.py Ollama reachability 체크(최소)
|
|
│ ├─ tests/
|
|
│ │ ├─ __init__.py
|
|
│ │ ├─ conftest.py
|
|
│ │ └─ test_health.py
|
|
│ ├─ migrations/ (alembic init 산출물)
|
|
│ ├─ alembic.ini
|
|
│ ├─ .env OLLAMA_HOST/OLLAMA_MODEL/DATABASE_URL/FRONTEND_ORIGIN
|
|
│ ├─ pyproject.toml
|
|
│ └─ ruff.toml (또는 pyproject 내 [tool.ruff])
|
|
├─ Makefile 동시 실행/공통 명령
|
|
├─ .gitignore
|
|
└─ README.md 실행법 갱신
|
|
```
|
|
|
|
---
|
|
|
|
## 3. 상세 구현 (파일별, 단계별)
|
|
|
|
### 3.0 레포 초기화 & .gitignore
|
|
|
|
```bash
|
|
# 레포 루트로 이동 (cwd 가정)
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace
|
|
git init
|
|
```
|
|
|
|
루트 `.gitignore`:
|
|
|
|
```gitignore
|
|
# ── Node / Next.js ──
|
|
frontend/node_modules/
|
|
frontend/.next/
|
|
frontend/out/
|
|
frontend/.env.local
|
|
frontend/coverage/
|
|
frontend/playwright-report/
|
|
frontend/test-results/
|
|
|
|
# ── Python / FastAPI ──
|
|
backend/.venv/
|
|
backend/__pycache__/
|
|
backend/**/__pycache__/
|
|
backend/.env
|
|
backend/ari.db
|
|
backend/.pytest_cache/
|
|
backend/.ruff_cache/
|
|
*.pyc
|
|
|
|
# ── 공통 ──
|
|
.DS_Store
|
|
*.log
|
|
```
|
|
|
|
> `design-reference/`는 픽셀 충실 재현 기준이므로 **절대 .gitignore에 넣지 않는다**(추적 유지).
|
|
|
|
---
|
|
|
|
### 3.1 Frontend — `create-next-app`
|
|
|
|
루트에서 비대화식으로 생성한다(App Router + TypeScript + ESLint, `src/` 미사용, import alias `@/*`).
|
|
|
|
```bash
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace
|
|
pnpm create next-app@latest frontend \
|
|
--ts \
|
|
--eslint \
|
|
--app \
|
|
--no-src-dir \
|
|
--import-alias "@/*" \
|
|
--use-pnpm \
|
|
--no-tailwind \
|
|
--no-turbopack
|
|
```
|
|
|
|
> Tailwind는 쓰지 않는다. 디자인은 원본 CSS 토큰(`design-reference/assets/dash.css :root`)을 그대로 이식하는 **순수 CSS 변수 + CSS 모듈** 전략이다(Phase 1). 그래서 `--no-tailwind`.
|
|
|
|
#### 3.1.1 폴더 구조 보강
|
|
|
|
create-next-app이 만든 `app/`에 더해 다음 디렉터리를 만든다.
|
|
|
|
```bash
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace/frontend
|
|
mkdir -p components lib lib/hooks styles tests playwright
|
|
touch components/.gitkeep lib/hooks/.gitkeep playwright/.gitkeep
|
|
```
|
|
|
|
#### 3.1.2 `app/layout.tsx` — 앱 셸 자리표시자 + 폰트 로딩
|
|
|
|
```tsx
|
|
// frontend/app/layout.tsx
|
|
import type { Metadata } from "next";
|
|
import "@/styles/tokens.css";
|
|
import "@/styles/globals.css";
|
|
|
|
export const metadata: Metadata = {
|
|
title: "아리 — AI LIFE OS",
|
|
description: "적을 때는 분류하지 않는다 — 분류·배치·자동화는 아리가.",
|
|
};
|
|
|
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<html lang="ko" data-theme="light">
|
|
<head>
|
|
{/* 원본 대시보드.html 의 폰트 로딩을 그대로 옮긴다 */}
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Onest:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css"
|
|
/>
|
|
</head>
|
|
<body>{children}</body>
|
|
</html>
|
|
);
|
|
}
|
|
```
|
|
|
|
> `data-theme="light"`는 원본 `대시보드.html`의 `<html lang="ko" data-theme="light">`와 동일(2행). 라이트/다크 토글 + localStorage 영속(next-themes)은 Phase 1에서 붙인다. Phase 0에선 정적 `light` 고정으로 충분.
|
|
|
|
#### 3.1.3 `app/page.tsx` — `/` → `/dashboard` 리다이렉트(자리표시자)
|
|
|
|
CONTRACT의 라우팅 규칙(`/ → /dashboard`)을 지금부터 박아둔다. 단 `dashboard` 라우트 실체는 Phase 5에서 만들므로, Phase 0에서는 리다이렉트만 두고 임시 안내 페이지를 둔다.
|
|
|
|
```tsx
|
|
// frontend/app/page.tsx
|
|
import { redirect } from "next/navigation";
|
|
|
|
export default function Home() {
|
|
redirect("/dashboard");
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
// frontend/app/dashboard/page.tsx (Phase 0 임시 자리표시자 — Phase 5에서 교체)
|
|
export default function DashboardPlaceholder() {
|
|
return (
|
|
<main style={{ padding: 40 }}>
|
|
<h1>아리 — 대시보드 (준비 중)</h1>
|
|
<p>Phase 0 스캐폴딩. 실제 대시보드는 phase-5-dashboard.md 에서 구현됩니다.</p>
|
|
</main>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### 3.1.4 `styles/tokens.css` 자리표시자 + `styles/globals.css`
|
|
|
|
Phase 1에서 전체 `:root` 토큰을 이식한다. Phase 0에서는 **body 기본 규칙과 폰트 변수만** 둬서 한국어 렌더가 깨지지 않게 한다.
|
|
|
|
```css
|
|
/* frontend/styles/tokens.css — Phase 1에서 design-reference/assets/dash.css :root 전체 이식 */
|
|
:root {
|
|
--font-disp: "Onest", "Pretendard", system-ui, sans-serif;
|
|
--font-ui: "Pretendard", "Onest", system-ui, sans-serif;
|
|
--font-mono: "DM Mono", ui-monospace, monospace;
|
|
/* 색/그림자/글래스 토큰은 phase-1-design-system.md 에서 채운다 */
|
|
}
|
|
```
|
|
|
|
```css
|
|
/* frontend/styles/globals.css */
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; padding: 0; }
|
|
|
|
body {
|
|
font-family: var(--font-ui);
|
|
-webkit-font-smoothing: antialiased;
|
|
letter-spacing: -0.011em; /* 원본 dash.css body 규칙 */
|
|
word-break: keep-all; /* 한국어 가독성 */
|
|
min-height: 100vh;
|
|
}
|
|
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
```
|
|
|
|
> 위 값들은 추측이 아니라 원본 `dash.css` 79~94행과 49~51행에서 그대로 가져온 것이다. 배경 그라데이션(`linear-gradient(178deg, ...)`)·색 토큰은 Phase 1에서 추가한다.
|
|
|
|
#### 3.1.5 `lib/api.ts` — API 베이스 URL 헬퍼
|
|
|
|
```ts
|
|
// frontend/lib/api.ts
|
|
export const API_BASE =
|
|
process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8000";
|
|
|
|
/** /api 프리픽스 경로를 절대 URL로. 예: apiUrl("/health") → http://localhost:8000/api/health */
|
|
export function apiUrl(path: string): string {
|
|
const clean = path.startsWith("/") ? path : `/${path}`;
|
|
return `${API_BASE}/api${clean}`;
|
|
}
|
|
|
|
/** 얇은 fetch 래퍼 — Phase 2 이후 본격 사용. Phase 0 스모크용 health 핑. */
|
|
export async function getJson<T>(path: string): Promise<T> {
|
|
const res = await fetch(apiUrl(path), { headers: { Accept: "application/json" } });
|
|
if (!res.ok) throw new Error(`GET ${path} → ${res.status}`);
|
|
return res.json() as Promise<T>;
|
|
}
|
|
```
|
|
|
|
#### 3.1.6 `lib/types.ts` 자리표시자
|
|
|
|
```ts
|
|
// frontend/lib/types.ts
|
|
// Phase 2에서 backend/app/schemas.py 와 1:1 대응하는 타입을 채운다.
|
|
// (person / folder / project / task / inbox_item / classification ...)
|
|
export {};
|
|
```
|
|
|
|
#### 3.1.7 `.env.local`
|
|
|
|
```bash
|
|
# frontend/.env.local (커밋 금지 — .gitignore 처리됨)
|
|
NEXT_PUBLIC_API_BASE=http://localhost:8000
|
|
```
|
|
|
|
> CONTRACT 명명 규칙대로 환경변수명은 `NEXT_PUBLIC_API_BASE`. Next.js는 `NEXT_PUBLIC_` 접두 변수만 클라이언트 번들에 노출한다.
|
|
|
|
#### 3.1.8 `package.json` 스크립트
|
|
|
|
create-next-app 기본 스크립트에 vitest/format을 더한다.
|
|
|
|
```jsonc
|
|
// frontend/package.json (scripts 부분)
|
|
{
|
|
"scripts": {
|
|
"dev": "next dev -p 3000",
|
|
"build": "next build",
|
|
"start": "next start -p 3000",
|
|
"lint": "next lint",
|
|
"format": "prettier --write .",
|
|
"format:check": "prettier --check .",
|
|
"test": "vitest run",
|
|
"test:watch": "vitest",
|
|
"e2e": "playwright test"
|
|
}
|
|
}
|
|
```
|
|
|
|
devDependencies 추가 설치:
|
|
|
|
```bash
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace/frontend
|
|
pnpm add -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom prettier
|
|
# Playwright는 Phase 6에서 본격 사용하지만 자리만 잡아둔다(선택):
|
|
# pnpm add -D @playwright/test && pnpm exec playwright install --with-deps
|
|
```
|
|
|
|
#### 3.1.9 `vitest.config.ts` + 빈 스모크 테스트
|
|
|
|
```ts
|
|
// frontend/vitest.config.ts
|
|
import { defineConfig } from "vitest/config";
|
|
import react from "@vitejs/plugin-react";
|
|
import path from "node:path";
|
|
|
|
export default defineConfig({
|
|
plugins: [react()],
|
|
test: {
|
|
environment: "jsdom",
|
|
globals: true,
|
|
include: ["tests/**/*.test.{ts,tsx}"],
|
|
},
|
|
resolve: {
|
|
alias: { "@": path.resolve(__dirname, ".") },
|
|
},
|
|
});
|
|
```
|
|
|
|
```ts
|
|
// frontend/tests/smoke.test.ts
|
|
import { describe, it, expect } from "vitest";
|
|
import { apiUrl } from "@/lib/api";
|
|
|
|
describe("phase-0 smoke", () => {
|
|
it("apiUrl 이 /api 프리픽스를 붙인다", () => {
|
|
expect(apiUrl("/health")).toMatch(/\/api\/health$/);
|
|
});
|
|
});
|
|
```
|
|
|
|
#### 3.1.10 Prettier 설정
|
|
|
|
```jsonc
|
|
// frontend/.prettierrc
|
|
{
|
|
"semi": true,
|
|
"singleQuote": false,
|
|
"printWidth": 100,
|
|
"trailingComma": "all"
|
|
}
|
|
```
|
|
|
|
> ESLint는 create-next-app이 `eslint.config.mjs`(또는 `.eslintrc.json`)로 이미 생성한다. 그대로 둔다(`next/core-web-vitals`).
|
|
|
|
---
|
|
|
|
### 3.2 Backend — FastAPI + SQLite + Ollama 핑
|
|
|
|
#### 3.2.1 패키지 매니저 부트스트랩(uv 권장 / pip 폴백)
|
|
|
|
**uv 사용 시:**
|
|
|
|
```bash
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace
|
|
mkdir backend
|
|
cd backend
|
|
uv init --no-readme --python 3.11
|
|
uv add fastapi "uvicorn[standard]" sqlmodel alembic httpx pydantic-settings python-dotenv
|
|
uv add --dev pytest ruff black
|
|
```
|
|
|
|
**pip + venv 폴백:**
|
|
|
|
```bash
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace/backend
|
|
python3 -m venv .venv
|
|
source .venv/bin/activate
|
|
pip install fastapi "uvicorn[standard]" sqlmodel alembic httpx pydantic-settings python-dotenv
|
|
pip install pytest ruff black
|
|
pip freeze > requirements.txt
|
|
```
|
|
|
|
#### 3.2.2 `pyproject.toml`
|
|
|
|
```toml
|
|
# backend/pyproject.toml
|
|
[project]
|
|
name = "ari-backend"
|
|
version = "0.1.0"
|
|
description = "아리 — AI LIFE OS 백엔드 (FastAPI + SQLite)"
|
|
requires-python = ">=3.11"
|
|
dependencies = [
|
|
"fastapi",
|
|
"uvicorn[standard]",
|
|
"sqlmodel",
|
|
"alembic",
|
|
"httpx",
|
|
"pydantic-settings",
|
|
"python-dotenv",
|
|
]
|
|
|
|
[dependency-groups]
|
|
dev = ["pytest", "ruff", "black"]
|
|
|
|
[tool.ruff]
|
|
line-length = 100
|
|
target-version = "py311"
|
|
|
|
[tool.ruff.lint]
|
|
select = ["E", "F", "I", "UP", "B"]
|
|
|
|
[tool.black]
|
|
line-length = 100
|
|
|
|
[tool.pytest.ini_options]
|
|
testpaths = ["tests"]
|
|
addopts = "-q"
|
|
```
|
|
|
|
#### 3.2.3 `app/config.py` — 환경변수 settings
|
|
|
|
```python
|
|
# backend/app/config.py
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
|
|
|
# ── DB ──
|
|
database_url: str = "sqlite:///./ari.db"
|
|
|
|
# ── CORS ──
|
|
frontend_origin: str = "http://localhost:3000"
|
|
|
|
# ── Ollama (모델 비종속: 모델명은 환경변수로 주입) ──
|
|
ollama_host: str = "http://localhost:11434"
|
|
ollama_model: str = "llama3.1" # 예시 기본값 — 강제 아님, 설치된 모델로 덮어쓰기
|
|
|
|
|
|
settings = Settings()
|
|
```
|
|
|
|
> CONTRACT: "특정 모델에 종속되지 않게 … 모델명은 환경변수(OLLAMA_MODEL)로 주입". 여기 `ollama_model` 기본값은 단지 자리표시 예시이며, `.env`의 `OLLAMA_MODEL`로 항상 덮어쓸 수 있다. 문서/코드 어디에서도 특정 모델을 강제하지 않는다.
|
|
|
|
#### 3.2.4 `app/db.py` — SQLite 엔진/세션
|
|
|
|
```python
|
|
# backend/app/db.py
|
|
from collections.abc import Generator
|
|
|
|
from sqlmodel import Session, create_engine
|
|
|
|
from app.config import settings
|
|
|
|
# SQLite + 단일 프로세스 dev: check_same_thread=False 필요
|
|
engine = create_engine(
|
|
settings.database_url,
|
|
echo=False,
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
|
|
|
|
def get_session() -> Generator[Session, None, None]:
|
|
with Session(engine) as session:
|
|
yield session
|
|
```
|
|
|
|
> 테이블 생성(`SQLModel.metadata.create_all`)·시드는 Phase 0이 아니라 `phase-2-backend.md`에서. 여기선 엔진/세션 의존성만 준비. `DATABASE_URL=sqlite:///./ari.db`는 CONTRACT 고정값(상대 경로 → `backend/ari.db` 생성).
|
|
|
|
#### 3.2.5 `app/llm/ollama.py` — reachability 체크(최소)
|
|
|
|
Phase 0에서는 **추상 Provider 인터페이스 전체를 구현하지 않는다**(그건 `phase-2-backend.md`). 다만 "Ollama가 살아 있는가 + 어떤 모델을 쓸 것인가"만 반환하는 얇은 함수를 둔다.
|
|
|
|
```python
|
|
# backend/app/llm/ollama.py
|
|
import httpx
|
|
|
|
from app.config import settings
|
|
|
|
|
|
async def check_ollama() -> dict:
|
|
"""Ollama 데몬 reachability + 설치 모델 목록을 비차단으로 점검.
|
|
Ollama 가 꺼져 있어도 예외를 삼키고 reachable=False 로 반환한다(서버는 죽지 않음).
|
|
"""
|
|
url = f"{settings.ollama_host}/api/tags"
|
|
try:
|
|
async with httpx.AsyncClient(timeout=2.0) as client:
|
|
res = await client.get(url)
|
|
res.raise_for_status()
|
|
data = res.json()
|
|
models = [m.get("name") for m in data.get("models", [])]
|
|
return {
|
|
"reachable": True,
|
|
"host": settings.ollama_host,
|
|
"model": settings.ollama_model,
|
|
"model_installed": settings.ollama_model in models,
|
|
"available_models": models,
|
|
}
|
|
except Exception as exc: # 연결 거부/타임아웃/JSON 오류 모두 포함
|
|
return {
|
|
"reachable": False,
|
|
"host": settings.ollama_host,
|
|
"model": settings.ollama_model,
|
|
"model_installed": False,
|
|
"available_models": [],
|
|
"error": type(exc).__name__,
|
|
}
|
|
```
|
|
|
|
> 타임아웃 2초로 짧게: Ollama가 꺼져 있을 때 `/api/llm/health`가 오래 매달리지 않도록. `model_installed`로 "데몬은 떴지만 주입한 모델이 아직 `ollama pull` 안 됐다"를 구분해 표시한다.
|
|
|
|
#### 3.2.6 라우터 — `app/routers/health.py`, `app/routers/llm.py`
|
|
|
|
> **라우터 프리픽스 전략(정본):** 각 라우터는 **내부 prefix 없이** 경로를 정의한다(`APIRouter()` + `@router.get("/health")`). `/api` 프리픽스는 오직 `main.py`의 `app.include_router(router, prefix="/api", tags=...)`에서 한 번만 붙인다. 라우터 안에서 `APIRouter(prefix="/api")`를 쓰지 않는다 — 이 규약은 이후 모든 Phase(특히 phase-2-backend.md)에서 동일하게 적용된다.
|
|
|
|
```python
|
|
# backend/app/routers/health.py
|
|
from fastapi import APIRouter
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/health")
|
|
async def health() -> dict:
|
|
return {"status": "ok"}
|
|
```
|
|
|
|
```python
|
|
# backend/app/routers/llm.py
|
|
from fastapi import APIRouter
|
|
|
|
from app.llm.ollama import check_ollama
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/llm/health")
|
|
async def llm_health() -> dict:
|
|
return await check_ollama()
|
|
```
|
|
|
|
#### 3.2.7 `app/main.py` — 앱 인스턴스 + CORS + 라우터 등록
|
|
|
|
```python
|
|
# backend/app/main.py
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
from app.config import settings
|
|
from app.routers import health, llm
|
|
|
|
app = FastAPI(title="아리 — AI LIFE OS API", version="0.1.0")
|
|
|
|
# CORS: 프론트 origin 허용
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[settings.frontend_origin],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# 모든 API 는 /api 프리픽스 (CONTRACT)
|
|
# 라우터는 내부 prefix 없이 경로를 정의하고, /api 프리픽스는 여기서만 붙인다 → 최종 /api/health, /api/llm/health
|
|
app.include_router(health.router, prefix="/api", tags=["health"])
|
|
app.include_router(llm.router, prefix="/api", tags=["llm"])
|
|
```
|
|
|
|
> `__init__.py` 파일들(`app/__init__.py`, `app/routers/__init__.py`, `app/llm/__init__.py`)을 빈 파일로 만들어 패키지로 인식시킨다.
|
|
|
|
#### 3.2.8 `.env`
|
|
|
|
```bash
|
|
# backend/.env (커밋 금지)
|
|
DATABASE_URL=sqlite:///./ari.db
|
|
FRONTEND_ORIGIN=http://localhost:3000
|
|
OLLAMA_HOST=http://localhost:11434
|
|
OLLAMA_MODEL=llama3.1 # ← 설치된 모델로 자유롭게 교체 (모델 비종속)
|
|
```
|
|
|
|
#### 3.2.9 alembic 초기화
|
|
|
|
```bash
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace/backend
|
|
uv run alembic init migrations # (pip 환경이면: alembic init migrations)
|
|
```
|
|
|
|
생성된 `alembic.ini`의 `sqlalchemy.url`은 코드 settings에서 주입하도록 `migrations/env.py`를 살짝 고친다(실제 마이그레이션 작성은 `phase-2-backend.md`).
|
|
|
|
```python
|
|
# backend/migrations/env.py (상단 부근에 추가)
|
|
from app.config import settings
|
|
config.set_main_option("sqlalchemy.url", settings.database_url)
|
|
# (SQLModel 메타데이터 target_metadata 연결은 phase-2 에서 모델 정의 후)
|
|
```
|
|
|
|
#### 3.2.10 백엔드 테스트 골격
|
|
|
|
```python
|
|
# backend/tests/conftest.py
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.main import app
|
|
|
|
|
|
@pytest.fixture()
|
|
def client() -> TestClient:
|
|
return TestClient(app)
|
|
```
|
|
|
|
```python
|
|
# backend/tests/test_health.py
|
|
def test_health(client):
|
|
res = client.get("/api/health")
|
|
assert res.status_code == 200
|
|
assert res.json() == {"status": "ok"}
|
|
|
|
|
|
def test_llm_health_shape(client):
|
|
"""Ollama on/off 무관하게 200 + 필수 키를 반환해야 한다(서버가 죽지 않음)."""
|
|
res = client.get("/api/llm/health")
|
|
assert res.status_code == 200
|
|
body = res.json()
|
|
assert "reachable" in body
|
|
assert "model" in body
|
|
assert "host" in body
|
|
```
|
|
|
|
> `test_llm_health_shape`는 Ollama가 꺼져 있어도 통과해야 한다(reachable이 true든 false든 키 존재만 검사). 이것이 "오프라인에서도 죽지 않는다"의 회귀 테스트다.
|
|
|
|
---
|
|
|
|
### 3.3 루트 `Makefile` — 동시 실행 & 공통 명령
|
|
|
|
```makefile
|
|
# Makefile (레포 루트)
|
|
.PHONY: help install fe be dev test lint format ollama-up health clean
|
|
|
|
FRONT := frontend
|
|
BACK := backend
|
|
|
|
help:
|
|
@echo "make install - 프론트/백 의존성 설치"
|
|
@echo "make dev - 프론트(3000)+백(8000) 동시 실행"
|
|
@echo "make fe - 프론트만 (next dev)"
|
|
@echo "make be - 백엔드만 (uvicorn --reload)"
|
|
@echo "make test - pytest + vitest"
|
|
@echo "make lint - ruff + next lint"
|
|
@echo "make health - 두 health 엔드포인트 curl"
|
|
|
|
install:
|
|
cd $(FRONT) && pnpm install
|
|
cd $(BACK) && uv sync
|
|
|
|
fe:
|
|
cd $(FRONT) && pnpm dev
|
|
|
|
be:
|
|
cd $(BACK) && uv run uvicorn app.main:app --reload --port 8000
|
|
|
|
# 두 서버 동시 실행: 백그라운드 백엔드 + 포그라운드 프론트. Ctrl-C 시 백엔드도 정리.
|
|
dev:
|
|
@echo "▶ backend :8000 / frontend :3000 (Ctrl-C 로 종료)"
|
|
@trap 'kill 0' INT TERM EXIT; \
|
|
( cd $(BACK) && uv run uvicorn app.main:app --reload --port 8000 ) & \
|
|
( cd $(FRONT) && pnpm dev ) & \
|
|
wait
|
|
|
|
test:
|
|
cd $(BACK) && uv run pytest
|
|
cd $(FRONT) && pnpm test
|
|
|
|
lint:
|
|
cd $(BACK) && uv run ruff check .
|
|
cd $(FRONT) && pnpm lint
|
|
|
|
format:
|
|
cd $(BACK) && uv run ruff format . && uv run black .
|
|
cd $(FRONT) && pnpm format
|
|
|
|
health:
|
|
@echo "── /api/health ──" && curl -s http://localhost:8000/api/health | python3 -m json.tool
|
|
@echo "── /api/llm/health ──" && curl -s http://localhost:8000/api/llm/health | python3 -m json.tool
|
|
|
|
clean:
|
|
rm -rf $(FRONT)/.next $(BACK)/ari.db $(BACK)/.pytest_cache
|
|
```
|
|
|
|
> pip+venv 환경이면 `uv run`을 `. .venv/bin/activate &&`로 바꾸거나, `uv run`을 빈 prefix로 두고 `source` 후 실행한다. uv 사용을 기본으로 둔다(CONTRACT 권장).
|
|
>
|
|
> **동시 실행 대안(npm 진영 도구):** `concurrently`를 루트에 두고 싶다면 `pnpm add -Dw concurrently` 후 루트 `package.json`에 `"dev": "concurrently -n be,fe -c blue,green \"make be\" \"make fe\""`를 둘 수 있다. 다만 Makefile만으로 충분하므로 추가 의존성은 선택.
|
|
|
|
---
|
|
|
|
### 3.4 품질 도구 (선택: pre-commit)
|
|
|
|
루트에 `.pre-commit-config.yaml`을 두면 커밋 전 lint/format을 강제할 수 있다.
|
|
|
|
```yaml
|
|
# .pre-commit-config.yaml (선택)
|
|
repos:
|
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
rev: v0.6.0
|
|
hooks:
|
|
- id: ruff
|
|
args: [--fix]
|
|
files: ^backend/
|
|
- id: ruff-format
|
|
files: ^backend/
|
|
- repo: local
|
|
hooks:
|
|
- id: frontend-lint
|
|
name: next lint
|
|
entry: bash -c 'cd frontend && pnpm lint'
|
|
language: system
|
|
files: ^frontend/
|
|
pass_filenames: false
|
|
```
|
|
|
|
설치:
|
|
|
|
```bash
|
|
pip install pre-commit && pre-commit install
|
|
```
|
|
|
|
| 영역 | 도구 | 명령 | 설정 위치 |
|
|
|---|---|---|---|
|
|
| Python lint | ruff | `uv run ruff check .` | `pyproject.toml [tool.ruff]` |
|
|
| Python format | black / ruff format | `uv run black .` | `pyproject.toml [tool.black]` |
|
|
| TS/React lint | ESLint(next) | `pnpm lint` | `eslint.config.mjs` |
|
|
| TS/React format | Prettier | `pnpm format` | `.prettierrc` |
|
|
| 커밋 게이트 | pre-commit (선택) | `pre-commit run -a` | `.pre-commit-config.yaml` |
|
|
|
|
---
|
|
|
|
## 4. 데이터/타입/API 계약 (이 Phase 관련 부분)
|
|
|
|
Phase 0에서 실제로 노출되는 계약은 **health 2종**뿐이다(나머지는 `phase-2-backend.md`). 모든 엔드포인트는 `/api` 프리픽스.
|
|
|
|
### 4.1 `GET /api/health`
|
|
|
|
응답(200):
|
|
|
|
```json
|
|
{ "status": "ok" }
|
|
```
|
|
|
|
### 4.2 `GET /api/llm/health`
|
|
|
|
CONTRACT: `GET /api/llm/health → {reachable, model, ...}`.
|
|
|
|
**Ollama 켜짐 + 모델 설치됨(예시):**
|
|
|
|
```json
|
|
{
|
|
"reachable": true,
|
|
"host": "http://localhost:11434",
|
|
"model": "llama3.1",
|
|
"model_installed": true,
|
|
"available_models": ["llama3.1", "qwen2.5"]
|
|
}
|
|
```
|
|
|
|
**Ollama 켜짐 + 주입 모델 미설치:**
|
|
|
|
```json
|
|
{
|
|
"reachable": true,
|
|
"host": "http://localhost:11434",
|
|
"model": "llama3.1",
|
|
"model_installed": false,
|
|
"available_models": ["qwen2.5"]
|
|
}
|
|
```
|
|
|
|
**Ollama 꺼짐(오프라인):**
|
|
|
|
```json
|
|
{
|
|
"reachable": false,
|
|
"host": "http://localhost:11434",
|
|
"model": "llama3.1",
|
|
"model_installed": false,
|
|
"available_models": [],
|
|
"error": "ConnectError"
|
|
}
|
|
```
|
|
|
|
> `model` 값은 환경변수 `OLLAMA_MODEL`을 그대로 비춘다. 위 `"llama3.1"`은 **예시일 뿐 강제 아님** — 사용자가 `.env`에서 설치된 모델로 바꾸면 그 값이 그대로 노출된다(모델 추상화 원칙).
|
|
|
|
### 4.3 Phase 2 이후 채워질 계약(참고만)
|
|
|
|
Phase 0은 아래를 **구현하지 않는다**. `frontend/lib/types.ts`·`backend/app/schemas.py`의 1:1 대응은 `phase-2-backend.md`에서 시작한다. 미리 인지만:
|
|
|
|
`GET /api/people`, `GET /api/tree`, `GET /api/tasks`, `GET /api/inbox`, `POST /api/inbox/capture`, `GET /api/dashboard`, `GET /api/risks` 등. 데이터 모델(person/folder/project/task/inbox_item/inbox_classification …)도 Phase 2.
|
|
|
|
---
|
|
|
|
## 5. 디자인 충실도 노트
|
|
|
|
Phase 0은 UI를 거의 그리지 않지만, **재현의 기준점**을 어긋나지 않게 못 박는다.
|
|
|
|
- **언어/테마 속성:** `<html lang="ko" data-theme="light">` — 원본 `design-reference/대시보드.html` 2행과 동일.
|
|
- **폰트:** Onest(디스플레이) / Pretendard(본문) / DM Mono(숫자). 원본 `대시보드.html` 8~17행의 `<link>`를 그대로 `layout.tsx`로 옮겼다.
|
|
- **body 기본 타이포:** `letter-spacing: -0.011em; word-break: keep-all;` — 원본 `dash.css` 86~87행. 한국어 줄바꿈이 자연스럽게 어절 단위로 끊긴다.
|
|
- **`.mono` 유틸:** `font-variant-numeric: tabular-nums` — 원본 `dash.css` 94행. 숫자 정렬용. 대시보드의 시간/금액(예: 예산 `1,280,000` / `2,000,000`, 일정 `09:30` 등)에서 쓰인다.
|
|
- **토큰 출처 명시:** `tokens.css`에 "Phase 1에서 `design-reference/assets/dash.css :root` 전체 이식" 주석을 남겨, 색 HEX(`--ink #211f1c`, `--lime #c2f24a`, `--blue #4f72e0` 등)·라운드(`--radius 22px`)·글래스(`--blur blur(26px) saturate(190%)`)가 임의값으로 새지 않게 한다.
|
|
- **앱 셸 메타:** 브랜드/내비(아리 · AI LIFE OS, 13항목 메인 내비)는 Phase 1에서 원본 `shell.jsx`의 `MAIN` 배열·`P` 아이콘 맵을 이식한다. Phase 0의 `metadata.title`만 "아리 — AI LIFE OS"로 맞춰둔다.
|
|
|
|
> 색·그림자·글래스·배경 그라데이션(`linear-gradient(178deg, var(--bg-top) 0%, var(--bg-mid) 20%, var(--bg-bot) 100%)`, `background-attachment: fixed`)을 Phase 0에서 넣지 않는 이유: 토큰 전체 세트를 한 번에 일관되게 이식하는 게 Phase 1의 책임이라 분산시키지 않기 위함. Phase 0의 임시 placeholder 페이지는 흰 배경이어도 무방.
|
|
|
|
---
|
|
|
|
## 6. 상태 처리(로딩/빈/에러/오프라인) & 엣지 케이스
|
|
|
|
Phase 0의 상태 처리는 거의 전부 **"Ollama가 꺼져 있어도 죽지 않는다"**에 집중된다.
|
|
|
|
| 상황 | 기대 동작 | 구현 포인트 |
|
|
|---|---|---|
|
|
| Ollama 데몬 꺼짐 | `/api/llm/health` 200 + `reachable:false` | `check_ollama`의 `except Exception` 폴백 |
|
|
| Ollama 떴지만 모델 미설치 | `reachable:true`, `model_installed:false` | `available_models`에 주입 모델 부재 |
|
|
| Ollama 응답 지연 | 2초 타임아웃 후 `reachable:false` | `httpx.AsyncClient(timeout=2.0)` |
|
|
| 백엔드 미기동인데 프론트 health 핑 | 프론트 fetch 실패 → 콘솔 경고, 페이지는 렌더 | `getJson`이 throw → 호출부에서 try/catch(Phase 1 배지에서) |
|
|
| CORS 차단 | 발생하면 안 됨 | `allow_origins=[frontend_origin]` 일치 확인 |
|
|
| SQLite 파일 미존재 | Phase 0에선 무관(테이블 생성 안 함) | `ari.db`는 Phase 2에서 생성 |
|
|
| 포트 충돌(3000/8000 사용 중) | 명시 에러 | dev 스크립트에서 포트 고정, 충돌 시 기존 프로세스 종료 안내 |
|
|
|
|
엣지 케이스 메모:
|
|
- `OLLAMA_MODEL`을 비워두면 `model: ""`로 나간다 → 프론트 배지는 "모델 미설정"으로 표시(Phase 1). Phase 0에선 빈 문자열 허용.
|
|
- `FRONTEND_ORIGIN`을 잘못 설정하면(예: 끝에 `/`) CORS preflight가 막힌다 → `.env`에 슬래시 없는 origin만.
|
|
|
|
---
|
|
|
|
## 7. 테스팅 & 검증 (가장 중요)
|
|
|
|
### 7.1 실행 명령 & 기대 출력
|
|
|
|
**(A) 백엔드 단독 부팅:**
|
|
|
|
```bash
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace/backend
|
|
uv run uvicorn app.main:app --reload --port 8000
|
|
```
|
|
기대 로그(요지):
|
|
```
|
|
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
INFO: Application startup complete.
|
|
```
|
|
|
|
**(B) health 엔드포인트 curl:**
|
|
|
|
```bash
|
|
curl -s http://localhost:8000/api/health
|
|
# → {"status":"ok"}
|
|
|
|
curl -s http://localhost:8000/api/llm/health | python3 -m json.tool
|
|
# Ollama on → {"reachable": true, "model": "...", "model_installed": ...}
|
|
# Ollama off → {"reachable": false, "model": "...", "error": "ConnectError"}
|
|
```
|
|
|
|
상태코드까지 확인:
|
|
```bash
|
|
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/api/health
|
|
# → 200
|
|
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/api/llm/health
|
|
# → 200 (Ollama on/off 모두)
|
|
```
|
|
|
|
**(C) Ollama on/off 양쪽 검증:**
|
|
|
|
```bash
|
|
# OFF 상태(데몬 미실행)에서 (B) 실행 → reachable:false, 200
|
|
# 그 다음 ON:
|
|
ollama serve # (백그라운드 데몬. 이미 떠 있으면 생략)
|
|
ollama pull <설치할_모델> # 예: .env 의 OLLAMA_MODEL 값과 맞춘다 (모델 강제 아님)
|
|
ollama list # 설치된 모델 확인 → available_models 와 대조
|
|
# 다시 (B) 실행 → reachable:true, model_installed:true
|
|
```
|
|
|
|
**(D) 프론트 dev 서버 부팅:**
|
|
|
|
```bash
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace/frontend
|
|
pnpm dev
|
|
```
|
|
기대 로그:
|
|
```
|
|
▲ Next.js 15.x
|
|
- Local: http://localhost:3000
|
|
✓ Ready in ...
|
|
```
|
|
브라우저로 `http://localhost:3000` → `/dashboard`로 리다이렉트되어 "아리 — 대시보드 (준비 중)" 표시.
|
|
|
|
**(E) 두 서버 동시 실행:**
|
|
|
|
```bash
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace
|
|
make dev
|
|
# 다른 터미널에서:
|
|
make health
|
|
```
|
|
|
|
**(F) 빈 테스트 통과:**
|
|
|
|
```bash
|
|
# 백엔드
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace/backend
|
|
uv run pytest
|
|
# 기대: 2 passed (test_health, test_llm_health_shape)
|
|
|
|
# 프론트
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace/frontend
|
|
pnpm test
|
|
# 기대: Test Files 1 passed | Tests 1 passed
|
|
```
|
|
|
|
**(G) lint 통과:**
|
|
|
|
```bash
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace/backend && uv run ruff check . # → All checks passed!
|
|
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace/frontend && pnpm lint # → ✔ No ESLint warnings or errors
|
|
```
|
|
|
|
### 7.2 구체 테스트 케이스 목록
|
|
|
|
| # | 케이스 | 방법 | 통과 기준 |
|
|
|---|---|---|---|
|
|
| T1 | `/api/health` 200 + 정확한 바디 | pytest `test_health` / curl | `{"status":"ok"}`, 200 |
|
|
| T2 | `/api/llm/health` 스키마(필수 키) | pytest `test_llm_health_shape` | `reachable/model/host` 키 존재, 200 |
|
|
| T3 | Ollama OFF에서 서버 미충돌 | 데몬 끈 채 curl (B) | 200 + `reachable:false`, 예외 누수 없음 |
|
|
| T4 | Ollama ON에서 reachable | `ollama serve` 후 curl | `reachable:true` |
|
|
| T5 | 주입 모델 설치/미설치 구분 | `.env`의 `OLLAMA_MODEL` vs `ollama list` | `model_installed` 정확 |
|
|
| T6 | CORS 허용 | 프론트에서 `getJson("/health")` | preflight/실요청 모두 통과(콘솔 CORS 에러 없음) |
|
|
| T7 | `/` → `/dashboard` 리다이렉트 | 브라우저/Playwright(선택) | URL이 `/dashboard`로 변경 |
|
|
| T8 | 프론트 빈 vitest | `pnpm test` | 1 passed |
|
|
| T9 | lint 클린(양쪽) | ruff / next lint | 경고·에러 0 |
|
|
| T10 | `make dev` 동시 부팅 | `make dev` + `make health` | 두 health 모두 응답 |
|
|
|
|
> T6 빠른 확인: 프론트 임시 페이지에 `useEffect(() => { getJson("/health").then(console.log).catch(console.error); }, [])`를 잠깐 넣고 브라우저 콘솔에서 `{status:"ok"}`가 찍히는지 본 뒤 제거. (정식 health 배지는 Phase 1.)
|
|
|
|
### 7.3 수동 QA 체크리스트
|
|
|
|
- [ ] `make install` 후 `frontend/node_modules`와 `backend/.venv`(또는 uv 환경)이 생성됐다.
|
|
- [ ] `make be` 단독으로 8000 포트가 뜨고 `/docs`(FastAPI 자동 문서)가 열린다.
|
|
- [ ] `make fe` 단독으로 3000 포트가 뜨고 루트가 `/dashboard`로 리다이렉트된다.
|
|
- [ ] `make dev`로 두 서버가 동시에 뜨고 Ctrl-C 한 번에 둘 다 정리된다.
|
|
- [ ] `make health`가 두 JSON을 예쁘게 출력한다.
|
|
- [ ] Ollama를 **끈 채로** `/api/llm/health`가 200 + `reachable:false`(서버 죽지 않음).
|
|
- [ ] Ollama를 **켠 채로** `/api/llm/health`가 `reachable:true`이고 `available_models`에 `ollama list` 결과가 보인다.
|
|
- [ ] `.env`의 `OLLAMA_MODEL`을 바꾸면 응답의 `model` 값이 그대로 바뀐다(모델 비종속 확인).
|
|
- [ ] 브라우저 콘솔에 CORS 에러가 없다.
|
|
- [ ] `uv run pytest` / `pnpm test`가 모두 green.
|
|
- [ ] `ruff check` / `pnpm lint`가 모두 clean.
|
|
- [ ] `.env`, `.env.local`, `ari.db`, `node_modules`, `.venv`가 `git status`에 추적되지 않는다.
|
|
|
|
### 7.4 통과 기준(요약)
|
|
|
|
위 T1~T10이 전부 통과 + 수동 체크리스트 전 항목 체크. 특히 **"Ollama OFF에서도 `/api/llm/health`가 200으로 응답하고 서버가 죽지 않는다"**가 핵심 합격선이다(오프라인 폴백 보장 — Phase 2의 HeuristicProvider 전제).
|
|
|
|
---
|
|
|
|
## 8. 완료 기준 (Definition of Done)
|
|
|
|
- [ ] 모노레포 골격(`frontend/`, `backend/`, 루트 `Makefile`·`.gitignore`·`README.md`)이 존재한다.
|
|
- [ ] `frontend`가 App Router + TS + ESLint로 생성되고 `app/components/lib/styles/tests` 폴더가 있다.
|
|
- [ ] `frontend/.env.local`에 `NEXT_PUBLIC_API_BASE=http://localhost:8000`.
|
|
- [ ] `pnpm dev`로 3000 포트가 뜨고 `/` → `/dashboard` 리다이렉트 동작.
|
|
- [ ] 백엔드가 FastAPI + uvicorn + sqlmodel + alembic + httpx + pytest로 구성되고 `app` 패키지 골격이 있다.
|
|
- [ ] `GET /api/health` → `{"status":"ok"}` (200).
|
|
- [ ] `GET /api/llm/health`가 Ollama on/off 모두에서 200으로 응답하고 `reachable`을 정확히 보고한다.
|
|
- [ ] CORS가 `http://localhost:3000`을 허용한다.
|
|
- [ ] `DATABASE_URL=sqlite:///./ari.db`, `db.py` 엔진/세션, alembic init 완료.
|
|
- [ ] `backend/.env`에 `OLLAMA_HOST/OLLAMA_MODEL/DATABASE_URL/FRONTEND_ORIGIN`이 있고, 모델은 특정 모델로 강제되지 않는다.
|
|
- [ ] `make dev`로 두 서버 동시 실행, `make health`로 양쪽 health 확인.
|
|
- [ ] ruff/black + eslint/prettier 설정 존재, lint 통과.
|
|
- [ ] 빈 pytest(2건)·vitest(1건) 통과.
|
|
- [ ] README에 실행법 갱신.
|
|
|
|
### README 갱신 스니펫(루트 `README.md`에 추가)
|
|
|
|
````markdown
|
|
## 빠른 시작 (Phase 0)
|
|
|
|
```bash
|
|
# 0) 사전 설치: Node20+/pnpm, Python3.11+/uv, Ollama
|
|
make install # 프론트+백 의존성
|
|
|
|
# 1) 환경변수
|
|
cp backend/.env.example backend/.env # 없으면 위 문서 3.2.8 참고로 작성
|
|
echo 'NEXT_PUBLIC_API_BASE=http://localhost:8000' > frontend/.env.local
|
|
|
|
# 2) (선택) Ollama
|
|
ollama serve &
|
|
ollama pull <설치할_모델> # .env 의 OLLAMA_MODEL 과 맞춤 (특정 모델 강제 아님)
|
|
|
|
# 3) 실행
|
|
make dev # 프론트 :3000 + 백 :8000
|
|
|
|
# 4) 스모크
|
|
make health # /api/health, /api/llm/health
|
|
make test # pytest + vitest
|
|
make lint # ruff + next lint
|
|
```
|
|
````
|
|
|
|
---
|
|
|
|
## 9. 다음 단계
|
|
|
|
Phase 0의 골격 위에 다음을 쌓는다.
|
|
|
|
- **다음 문서: `phase-1-design-system.md`** — 디자인 토큰 전체 이식(`design-reference/assets/dash.css :root` → `frontend/styles/tokens.css`), 앱 셸(Topbar 13항목 메인 내비 + 브랜드 "아리 / AI LIFE OS", 테마 토글 + localStorage 영속, `Icon` 중앙 paths 맵 이식 = 원본 `shell.jsx`의 `P`·`MAIN`), 레이아웃, MVP 외 페이지 "준비 중" placeholder 라우팅, 그리고 `/api/llm/health`를 읽어 Topbar에 Ollama 연결 상태 배지를 표시.
|
|
- 그 다음 `phase-2-backend.md`에서 데이터 모델·Alembic 마이그레이션·시드(`design-reference/assets`의 data.js·tasks-data.js·sinbox-data.js·approve-data.js 이식)·전체 REST API·Ollama Provider 추상화·분류/리스크 서비스를 구현한다.
|
|
- 빌드 순서: **0 → 1 → 2 → (3 작업 → 4 인박스 → 5 대시보드) → 6 통합**.
|