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.

39 KiB

Phase 0 — 프로젝트 스캐폴딩 & 개발 환경

한 줄 요약: 모노레포(frontend/ + backend/)를 초기화하고 Next.js · FastAPI · SQLite · Ollama 4개 축을 연결한 뒤, /api/health · /api/llm/health · 프론트 dev 서버 부팅으로 "스모크 검증"까지 끝내는 단계.

이 문서는 dev/ 문서 세트의 일부입니다 — 먼저 overview.md를 읽으세요. 이후 순서는 phase-1-design-system.mdphase-2-backend.md → (phase-3-tasks.mdphase-4-inbox.mdphase-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)이 로드하는 폰트가 곧 우리 앱 셸의 폰트이기 때문이다.

<!-- 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

# 레포 루트로 이동 (cwd 가정)
cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace
git init

루트 .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 @/*).

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/에 더해 다음 디렉터리를 만든다.

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 — 앱 셸 자리표시자 + 폰트 로딩

// 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에서는 리다이렉트만 두고 임시 안내 페이지를 둔다.

// frontend/app/page.tsx
import { redirect } from "next/navigation";

export default function Home() {
  redirect("/dashboard");
}
// 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 기본 규칙과 폰트 변수만 둬서 한국어 렌더가 깨지지 않게 한다.

/* 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 에서 채운다 */
}
/* 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 헬퍼

// 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 자리표시자

// 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

# 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을 더한다.

// 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 추가 설치:

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 + 빈 스모크 테스트

// 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, ".") },
  },
});
// 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 설정

// 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 사용 시:

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 폴백:

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

# 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

# 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 기본값은 단지 자리표시 예시이며, .envOLLAMA_MODEL로 항상 덮어쓸 수 있다. 문서/코드 어디에서도 특정 모델을 강제하지 않는다.

3.2.4 app/db.py — SQLite 엔진/세션

# 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가 살아 있는가 + 어떤 모델을 쓸 것인가"만 반환하는 얇은 함수를 둔다.

# 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.pyapp.include_router(router, prefix="/api", tags=...)에서 한 번만 붙인다. 라우터 안에서 APIRouter(prefix="/api")를 쓰지 않는다 — 이 규약은 이후 모든 Phase(특히 phase-2-backend.md)에서 동일하게 적용된다.

# backend/app/routers/health.py
from fastapi import APIRouter

router = APIRouter()


@router.get("/health")
async def health() -> dict:
    return {"status": "ok"}
# 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 + 라우터 등록

# 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

# 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 초기화

cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace/backend
uv run alembic init migrations   # (pip 환경이면: alembic init migrations)

생성된 alembic.inisqlalchemy.url은 코드 settings에서 주입하도록 migrations/env.py를 살짝 고친다(실제 마이그레이션 작성은 phase-2-backend.md).

# 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 백엔드 테스트 골격

# backend/tests/conftest.py
import pytest
from fastapi.testclient import TestClient

from app.main import app


@pytest.fixture()
def client() -> TestClient:
    return TestClient(app)
# 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 (레포 루트)
.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을 강제할 수 있다.

# .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

설치:

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):

{ "status": "ok" }

4.2 GET /api/llm/health

CONTRACT: GET /api/llm/health → {reachable, model, ...}.

Ollama 켜짐 + 모델 설치됨(예시):

{
  "reachable": true,
  "host": "http://localhost:11434",
  "model": "llama3.1",
  "model_installed": true,
  "available_models": ["llama3.1", "qwen2.5"]
}

Ollama 켜짐 + 주입 모델 미설치:

{
  "reachable": true,
  "host": "http://localhost:11434",
  "model": "llama3.1",
  "model_installed": false,
  "available_models": ["qwen2.5"]
}

Ollama 꺼짐(오프라인):

{
  "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.jsxMAIN 배열·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_ollamaexcept 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) 백엔드 단독 부팅:

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:

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"}

상태코드까지 확인:

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 양쪽 검증:

# 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 서버 부팅:

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) 두 서버 동시 실행:

cd /Users/kim1634/mycloud/personal/workspace/ari_assistant/workspace
make dev
# 다른 터미널에서:
make health

(F) 빈 테스트 통과:

# 백엔드
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 통과:

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 주입 모델 설치/미설치 구분 .envOLLAMA_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 installfrontend/node_modulesbackend/.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/healthreachable:true이고 available_modelsollama list 결과가 보인다.
  • .envOLLAMA_MODEL을 바꾸면 응답의 model 값이 그대로 바뀐다(모델 비종속 확인).
  • 브라우저 콘솔에 CORS 에러가 없다.
  • uv run pytest / pnpm test가 모두 green.
  • ruff check / pnpm lint가 모두 clean.
  • .env, .env.local, ari.db, node_modules, .venvgit 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.localNEXT_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/.envOLLAMA_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에 추가)

## 빠른 시작 (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 :rootfrontend/styles/tokens.css), 앱 셸(Topbar 13항목 메인 내비 + 브랜드 "아리 / AI LIFE OS", 테마 토글 + localStorage 영속, Icon 중앙 paths 맵 이식 = 원본 shell.jsxP·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 통합.