chore: Makefile→justfile + 개발 포트 unique화(31300/31800)

- Makefile 삭제 → justfile. `just run dev`(둘 다) / `just run fe` / `just run be`
  + install/test/e2e/lint/format/health/seed/migrate/clean 레시피
- 개발 포트를 흔한 dev 포트(3000/5173/8000/8080)와 충돌 없는 31xxx 대역으로:
  프론트 :3000→:31300, 백엔드 :8000→:31800. 전 참조 일괄 변경
  (package.json/next.config/lib api 기본값/playwright config·fixture/.env*.example/
   config.py FRONTEND_ORIGIN/scripts/dev.sh/CI/README)
- E2E 견고화: inbox/tasks/dashboard spec 에 beforeAll(resetSeed) 추가 →
  직렬 전체 실행에서 앞 spec 의 캡처 누적과 격리(결정성↑, 전체 39 e2e 29.7s)

검증(새 포트): 백엔드 pytest 63 · 프론트 vitest 69 · playwright 39 전부 통과,
tsc/eslint/ruff clean, build OK. 라이브: :31300↔:31800 CORS·SSR fetch·/ 리다이렉트 정상,
`just health`/`just run` 동작, 옛 포트 참조 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main
Claude 2 months ago
parent c4e86e06ec
commit b3d21f17d2

@ -40,7 +40,7 @@ jobs:
- uses: astral-sh/setup-uv@v3
- run: uv sync && uv run alembic upgrade head && uv run python -m app.seed
working-directory: backend
- run: ARI_ALLOW_TEST_RESET=1 LLM_PROVIDER=heuristic uv run uvicorn app.main:app --port 8000 &
- run: ARI_ALLOW_TEST_RESET=1 LLM_PROVIDER=heuristic uv run uvicorn app.main:app --port 31800 &
working-directory: backend
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4

@ -1,59 +0,0 @@
# Makefile (레포 루트)
.PHONY: help install fe be dev test lint format health seed migrate 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 + eslint"
@echo "make health - 두 health 엔드포인트 curl"
@echo "make seed - DB 시드 적재"
@echo "make migrate - alembic upgrade head"
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
seed:
cd $(BACK) && uv run python -m app.seed
migrate:
cd $(BACK) && uv run alembic upgrade head
clean:
rm -rf $(FRONT)/.next $(BACK)/ari.db $(BACK)/.pytest_cache

@ -15,6 +15,9 @@ workspace/
└─ design-reference/ 원본 프로토타입 (픽셀 충실 재현 기준)
```
> **개발 포트**: 흔한 dev 포트(3000/5173/8000/8080)와 충돌을 피하려고 **프론트 :31300 · 백엔드 :31800** 을 씁니다.
> 작업 러너는 `make` 대신 **`just`** 입니다(`justfile`).
## 스택
| 레이어 | 선택 |
@ -27,17 +30,17 @@ workspace/
## 빠른 시작
### 0) 사전 설치
Node ≥ 20 · pnpm · Python ≥ 3.11 · uv · (선택) Ollama
Node ≥ 20 · pnpm · Python ≥ 3.11 · uv · **just** · (선택) Ollama
### 1) 의존성 + 환경변수 + 시드
```bash
make install # frontend(pnpm) + backend(uv) 의존성
just install # frontend(pnpm) + backend(uv) 의존성
cp backend/.env.example backend/.env # 필요 시 OLLAMA_MODEL 등 수정
cp frontend/.env.local.example frontend/.env.local
cd backend && uv run alembic upgrade head && uv run python -m app.seed && cd ..
just migrate && just seed # alembic upgrade head + 시드 적재
```
### 2) (선택) Ollama — 모델 비종속
@ -53,20 +56,20 @@ ollama pull <설치할_모델> # backend/.env 의 OLLAMA_MODEL 과 일치
### 3) 실행
```bash
make dev # backend :8000 + frontend :3000 (또는 scripts/dev.sh)
# http://localhost:3000 → / 는 /dashboard 로 리다이렉트
just run dev # backend :31800 + frontend :31300 (개별: just run be / just run fe)
# http://localhost:31300 → / 는 /dashboard 로 리다이렉트
```
### 4) 스모크 / 테스트
```bash
make health # /api/health, /api/llm/health
make test # pytest + vitest
make lint # ruff + eslint
just health # /api/health, /api/llm/health
just test # pytest + vitest
just lint # ruff + eslint
# E2E (백엔드는 리셋 허용 + heuristic 으로 기동해야 federation/a11y/responsive 전부 통과)
ARI_ALLOW_TEST_RESET=1 LLM_PROVIDER=heuristic uv run --directory backend uvicorn app.main:app --port 8000 &
cd frontend && pnpm exec playwright test
ARI_ALLOW_TEST_RESET=1 LLM_PROVIDER=heuristic uv run --directory backend uvicorn app.main:app --port 31800 &
just e2e # = pnpm exec playwright test --workers=1
```
## 시드 초기화(데모 리셋)
@ -74,7 +77,7 @@ cd frontend && pnpm exec playwright test
```bash
cd backend && rm -f ari.db && uv run alembic upgrade head && uv run python -m app.seed
# 또는 E2E용(가드): ARI_ALLOW_TEST_RESET=1 로 기동 후
curl -X POST http://localhost:8000/api/_test/reset
curl -X POST http://localhost:31800/api/_test/reset
```
## 환경변수
@ -86,9 +89,9 @@ curl -X POST http://localhost:8000/api/_test/reset
| `LLM_PROVIDER` | backend env | `auto` | `auto`\|`ollama`\|`heuristic` (테스트/E2E는 `heuristic`) |
| `LLM_TIMEOUT` | backend env | `30` | 분류 1건 타임아웃(초). 초과 시 heuristic 폴백 |
| `DATABASE_URL` | backend `.env` | `sqlite:///./ari.db` | DB 경로 |
| `FRONTEND_ORIGIN` | backend `.env` | `http://localhost:3000` | CORS 허용 출처(콤마구분) |
| `FRONTEND_ORIGIN` | backend `.env` | `http://localhost:31300` | CORS 허용 출처(콤마구분) |
| `ARI_ALLOW_TEST_RESET` | backend env | (미설정) | `1`이면 `/api/_test/reset` 허용(E2E 전용, 운영 403) |
| `NEXT_PUBLIC_API_BASE` | frontend `.env.local` | `http://localhost:8000` | 프론트가 호출할 백엔드 베이스 |
| `NEXT_PUBLIC_API_BASE` | frontend `.env.local` | `http://localhost:31800` | 프론트가 호출할 백엔드 베이스 |
## 연합(federation) 흐름 — MVP의 핵심

@ -1,4 +1,4 @@
DATABASE_URL=sqlite:///./ari.db
FRONTEND_ORIGIN=http://localhost:3000
FRONTEND_ORIGIN=http://localhost:31300
OLLAMA_HOST=http://localhost:11434
OLLAMA_MODEL=llama3.1

@ -19,7 +19,8 @@ class Settings(BaseSettings):
risk_today: int = 8
# CORS 허용 출처(콤마구분). 단일 환경변수 규약: FRONTEND_ORIGIN.
frontend_origin: str = "http://localhost:3000"
# 개발 포트는 흔한 dev 포트와 충돌을 피해 31xxx 대역 사용(프론트 31300).
frontend_origin: str = "http://localhost:31300"
# 테스트 리셋 게이트 (테스트 환경에서만 시드 리셋 허용)
ari_allow_test_reset: bool = False

@ -1,7 +1,7 @@
// frontend/lib/api.ts
export const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8000";
export const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:31800";
/** /api 프리픽스 경로를 절대 URL로. 예: apiUrl("/health") → http://localhost:8000/api/health */
/** /api 프리픽스 경로를 절대 URL로. 예: apiUrl("/health") → http://localhost:31800/api/health */
export function apiUrl(path: string): string {
const clean = path.startsWith("/") ? path : `/${path}`;
return `${API_BASE}/api${clean}`;

@ -1,7 +1,7 @@
// frontend/lib/dashboard/api.ts
import type { Dashboard } from "@/lib/types";
const BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8000";
const BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:31800";
export async function getDashboard(signal?: AbortSignal): Promise<Dashboard> {
const res = await fetch(`${BASE}/api/dashboard`, { signal, cache: "no-store" });

@ -1,7 +1,7 @@
// frontend/lib/inbox/api.ts
import type { CaptureResult, ConfirmResult, InboxItem, RouteType } from "@/lib/types";
const BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8000";
const BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:31800";
export class ApiError extends Error {
constructor(

@ -1,9 +1,9 @@
import type { NextConfig } from "next";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8000";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:31800";
const nextConfig: NextConfig = {
// dev 프록시: 프론트의 상대 경로 /api/* 호출을 백엔드(:8000)로 전달.
// dev 프록시: 프론트의 상대 경로 /api/* 호출을 백엔드(:31800)로 전달.
// lib/api.ts 는 절대 URL(API_BASE)을 쓰지만, 상대 경로 사용 시에도 동작하도록 둔다.
async rewrites() {
return [

@ -3,9 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3000",
"dev": "next dev -p 31300",
"build": "next build",
"start": "next start -p 3000",
"start": "next start -p 31300",
"lint": "eslint",
"format": "prettier --write .",
"format:check": "prettier --check .",

@ -10,7 +10,7 @@ export default defineConfig({
retries: 1,
reporter: [["list"]],
use: {
baseURL: "http://localhost:3000",
baseURL: "http://localhost:31300",
trace: "on-first-retry",
navigationTimeout: 45_000,
actionTimeout: 15_000,
@ -18,7 +18,7 @@ export default defineConfig({
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
webServer: {
command: "pnpm dev",
url: "http://localhost:3000",
url: "http://localhost:31300",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},

@ -1,8 +1,12 @@
// frontend/playwright/dashboard.spec.ts
import { expect, test } from "@playwright/test";
import { resetSeed } from "./fixtures/seed-reset";
test.describe.configure({ mode: "serial" });
// 깨끗한 시드에서 시작(앞선 spec 의 변경과 격리).
test.beforeAll(resetSeed);
test("로드 → 집계가 시드 기준으로 표시", async ({ page }) => {
await page.goto("/dashboard");
await expect(page.getByText("아리 브리핑")).toBeVisible();

@ -1,7 +1,7 @@
// frontend/playwright/fixtures/seed-reset.ts
import { request } from "@playwright/test";
const API = process.env.API_BASE ?? "http://localhost:8000";
const API = process.env.API_BASE ?? "http://localhost:31800";
/** 백엔드 시드를 초기 상태로 되돌린다. ARI_ALLOW_TEST_RESET=1 일 때만 동작. */
export async function resetSeed(): Promise<void> {

@ -3,9 +3,13 @@
// 캡처는 인박스 시드를 변경하므로 단일 워커 직렬.
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
import { resetSeed } from "./fixtures/seed-reset";
test.describe.configure({ mode: "serial" });
// 전체 스위트(직렬)에서 앞선 spec 의 캡처 누적과 무관하게 깨끗한 시드에서 시작.
test.beforeAll(resetSeed);
test("캡처 → 분류 → 좋아요 → 작업 페이지에 등장(연합)", async ({ page }) => {
await page.goto("/inbox");
const input = page.getByLabel("인박스에 빠르게 캡처");

@ -1,9 +1,13 @@
// frontend/playwright/tasks.spec.ts
// 주의: 백엔드(:8000) 시드 DB 를 변경하는 테스트 포함 → 단일 워커 직렬 실행.
// 주의: 백엔드(:31800) 시드 DB 를 변경하는 테스트 포함 → 단일 워커 직렬 실행.
import { expect, test } from "@playwright/test";
import { resetSeed } from "./fixtures/seed-reset";
test.describe.configure({ mode: "serial" });
// 깨끗한 시드에서 시작(앞선 spec 의 변경과 격리).
test.beforeAll(resetSeed);
test("초기 진입: 업무 스코프 + 칸반 5컬럼 + 리스크 레이더", async ({ page }) => {
await page.goto("/tasks");
await expect(page.locator(".tree-row.folder.on .tree-name")).toHaveText("업무");

@ -0,0 +1,64 @@
# 아리 — 작업 러너 (make 대체). `just --list` 로 전체 보기.
# 개발 포트는 흔한 dev 포트(3000/5173/8000/8080)와 충돌을 피해 31xxx 대역 사용.
be_port := "31800" # 백엔드 API
fe_port := "31300" # 프론트(package.json `dev` 스크립트도 -p {{fe_port}})
# 기본: 사용 가능한 레시피 목록
default:
@just --list
# 의존성 설치 (프론트 pnpm + 백엔드 uv)
install:
cd frontend && pnpm install
cd backend && uv sync
# 실행: `just run dev`(둘 다) | `just run fe` | `just run be`
run target="dev":
#!/usr/bin/env bash
set -euo pipefail
case "{{target}}" in
dev)
echo "▶ backend :{{be_port}} / frontend :{{fe_port}} (Ctrl-C 로 종료)"
trap 'kill 0' INT TERM EXIT
( cd backend && uv run uvicorn app.main:app --reload --port {{be_port}} ) &
( cd frontend && pnpm dev ) &
wait ;;
fe) cd frontend && pnpm dev ;;
be) cd backend && uv run uvicorn app.main:app --reload --port {{be_port}} ;;
*) echo "알 수 없는 대상: {{target}} (dev|fe|be)"; exit 1 ;;
esac
# 테스트 (백엔드 pytest + 프론트 vitest)
test:
cd backend && uv run pytest
cd frontend && pnpm test
# E2E (Playwright). 백엔드는 ARI_ALLOW_TEST_RESET=1 LLM_PROVIDER=heuristic 로 기동해야 전부 통과.
e2e:
cd frontend && pnpm exec playwright test --workers=1
# 린트 (ruff + eslint) / 포맷
lint:
cd backend && uv run ruff check .
cd frontend && pnpm lint
format:
cd backend && uv run ruff format . && uv run black .
cd frontend && pnpm format
# health 엔드포인트 확인
health:
@echo "── /api/health ──" && curl -s http://localhost:{{be_port}}/api/health | python3 -m json.tool
@echo "── /api/llm/health ──" && curl -s http://localhost:{{be_port}}/api/llm/health | python3 -m json.tool
# DB 시드 / 마이그레이션
seed:
cd backend && uv run python -m app.seed
migrate:
cd backend && uv run alembic upgrade head
# 빌드 산출물 정리
clean:
rm -rf frontend/.next backend/ari.db backend/.pytest_cache

@ -1,9 +1,9 @@
#!/usr/bin/env bash
# 아리 — backend(:8000) + frontend(:3000) 동시 기동. Ctrl-C 로 둘 다 정리.
# 아리 — backend(:31800) + frontend(:31300) 동시 기동. Ctrl-C 로 둘 다 정리.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
trap 'kill 0' INT TERM EXIT
( cd "$ROOT/backend" && uv run uvicorn app.main:app --reload --port 8000 ) &
( cd "$ROOT/backend" && uv run uvicorn app.main:app --reload --port 31800 ) &
( cd "$ROOT/frontend" && pnpm dev ) &
wait

Loading…
Cancel
Save