22 KiB
로컬 개발 실행 가이드
Vignette(AI 심리상담 시뮬레이션 훈련 플랫폼)를 로컬에서 띄워서 테스트하기 위한 단계별 가이드다. 주 환경은 Windows 11 + PowerShell을 기준으로 하되, 셸 차이가 중요한 곳은 별도로 표시한다.
- 작업 디렉터리(저장소 루트):
D:\workspace\vignette - 모노레포 구성
apps/api— FastAPI 백엔드 (Python)apps/api/engine_gateway— AI 턴 생성용 엔진 게이트웨이(별도 프로세스, 포트 9099)apps/web— React 19 + Vite 프런트엔드infra— Docker Compose 스택(pgvector pg16 + api + web + proxy)scripts— 운영/점검 스크립트
핵심 구성요소: 학습자(상담수련생)가 AI 내담자 페르소나와 회기를 진행하고, 종료 후 회기 리뷰 피드백을 받는다.
백엔드는 페르소나 엔진 · 오케스트레이터 · 저항엔진 · 마스킹 게이트 · 음성 캐스케이드 · 회기 리뷰를 소유하고,
실제 AI 발화 생성은 engine_gateway(ENGINE_MODE 라우팅)가 담당한다. 단일 SoR은 Postgres16+pgvector이며,
DB가 없으면 인메모리 degraded 폴백으로 기동한다.
0. 가장 빠른 경로 — 한 커맨드 (권장)
게이트웨이(9099)+API(8000)+웹(5173)을 한 번에 깔끔히 (재)기동한다. Docker가 사용 가능하면
vignette-dev-db Postgres(pgvector) 컨테이너도 보장하고 health/role을 점검한다. 기존/고아
프로세스는 커맨드라인 기준으로 정리한 뒤 결정론적으로 띄운다(--reload 워처 불안정 회피).
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-up.ps1
# 종료: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1
# DB 컨테이너까지 멈출 때만: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1 -Db
- 진입점 http://localhost:5173 → 로그인 페이지에서 dev-login(아무
@hs.ac.kr, role learner/teacher/admin). - Docker가 있으면 기본적으로
127.0.0.1:55432DB 컨테이너를 사용한다. 새 컨테이너 생성 시POSTGRES_USER=vignette_owner, API용 앱 role은DATABASE_URL사용자로 분리해 RLS 검증 기반을 보존한다. Docker가 없거나-NoDb를 쓰면 in-memory degraded로 뜬다. - 로그는
.devlogs/(gitignore). 코드 수정 후에는 dev-up을 다시 실행해 재기동(reload 미사용). - 옵션:
-NoGateway(기존 gateway 보존),-NoWeb(기존 web 보존, API만 재기동),-NoDb(DB 컨테이너 보장 건너뜀). -NoGateway/-NoWeb를 쓰면 해당 컴포넌트의 stale 정리도 건너뛰고, API 정리는 지정한-ApiPortlistener만 대상으로 한다. public8001, Tailnet8010, local8000을 나눠 띄운 상태에서 API-only 재기동할 때 다른 포트를 건드리지 않는다.dev-down.ps1은 기본적으로 DB 컨테이너를 보존한다. 컨테이너도 멈추려면-Db를 명시한다.
스크립트는 uvicorn이 설치된 python을 자동 해석한다(시스템에 복수 python 공존 시 'python' 별칭이 uvicorn 없는 인터프리터를 가리킬 수 있음 — 이 함정 때문에 명시 해석함).
0.1 Tailscale PC/모바일 접속
같은 Tailnet에 로그인한 장비에서는 아래 명령으로 Tailnet 전용 HTTPS 접속점을 켠다.
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-tailscale-runtime.ps1
- 현재 머신의 접속 주소: https://alpaca-home.taile93291.ts.net
- 이 경로는
tailscale serve로https://alpaca-home.taile93291.ts.net→ 로컬 web127.0.0.1:5173을 연결한다. alpaca-home.taile93291.ts.net는 Vite 기본allowedHosts에도 포함되어 있어, 수동 Vite 기동에서도 Tailnet host-header 403이 나지 않아야 한다. 추가 Tailnet host는VITE_ALLOWED_HOSTS에 쉼표로 더한다.- API는 Tailnet 전용으로
127.0.0.1:8010에 뜨고, Vite proxy는VITE_API_PROXY_TARGET으로 이 포트를 본다. 기존 8000 reloader 잔류와 충돌하지 않기 위해 분리했다. - dev-login은
AUTH_DEV_LOGIN_EXTRA_ORIGINS에 Tailnet origin이 들어간 경우에만 dev 환경에서 열린다. prod에서는 열리지 않는다. - Tailnet/로컬 dev에서는 Google OAuth를 사용하지 않는다. OAuth redirect URI가 공개 API callback으로 고정된 동안에는 콜백이 로컬/Tailnet 세션이 아니라 공개 API 세션으로 돌아가므로, 로그인 화면은 Google 버튼을 비활성화하고 직접
/api/auth/login?provider=google을 열어도local_oauth_unavailable안내로 되돌린다.
수동 기동(대안)
DB·엔진 없이도 UI/로그인/페르소나/세션 생성까지는 동작한다(턴 생성만 불가).
- API —
apps/api에서python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 - 웹 —
apps/web에서npm run dev→ http://localhost:5173 - (선택) 엔진 게이트웨이 —
apps/api에서python -m uvicorn engine_gateway.gateway:app --host 127.0.0.1 --port 9099(실제 AI 턴 생성을 하려면 필요.claudeCLI 설치+로그인 전제)
아래에서 각 단계를 자세히 설명한다.
1. 사전 준비
- Python 3.11 (운영 스크립트가 Python 3.11 기준). 가상환경 권장.
- Node.js(최신 LTS) + npm —
apps/web용. - (선택) Docker Desktop —
infra/docker-compose.yml전체 스택을 띄울 때만. - (선택)
claudeCLI —ENGINE_MODE=claude_cli로 실제 턴 생성을 할 때. 설치 후 로그인되어 있어야 한다.
Python 의존성 설치 (apps/api)
# 저장소 루트에서
python -m venv .venv
.\.venv\Scripts\Activate.ps1 # PowerShell 활성화
python -m pip install -r apps\api\requirements.txt
apps/api/requirements.txt는 fastapi, uvicorn[standard], asyncpg, pydantic,
pydantic-settings, python-multipart, httpx, sse-starlette를 exact version으로 고정한다.
엔진 게이트웨이도 fastapi+uvicorn만 쓰므로 위 설치로 함께 충족된다.
(Presidio PII 마스킹은 선택 의존성이라 기본 제외 — 없으면 정규식 폴백으로 자동 degrade.)
웹 의존성 설치 (apps/web)
cd apps\web
npm install
2. API 서버 실행 (FastAPI)
2.1 환경설정(.env 자동 로드)
설정은 전부 env 주입이며 pydantic-settings가 작업 디렉터리의 .env를 자동 로드한다
(apps/api/app/config.py의 SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")).
즉 apps/api 디렉터리에서 uvicorn을 실행하면 apps/api/.env가 자동 반영된다.
로컬 dev에서 중요한 키(이미 apps/api/.env에 셋업되어 있거나, 없으면 .env.example 참고):
| 키 | 로컬 dev 값 | 의미 |
|---|---|---|
ENVIRONMENT |
dev |
dev여야 DB 폴백·dev-login·seed 폴백이 허용됨 |
DATABASE_URL |
postgresql://...@127.0.0.1:55432/vignette |
미연결 시 degraded 폴백(dev 한정) |
ENGINE_URL |
http://127.0.0.1:9099 |
엔진 게이트웨이 베이스 URL |
ENGINE_MODE |
claude_cli |
엔진 provider 라우팅 |
AUTH_DEV_LOGIN_ENABLED |
true |
dev-login 엔드포인트 활성화 |
AUTH_ALLOWED_EMAIL_DOMAINS |
["hs.ac.kr","twentyoz.kr"] |
기본 로그인 허용 이메일 도메인(dev-login 포함 검증). /admin/users에 미리 등록된 정확한 이메일은 도메인 밖이어도 예외로 로그인 가능 |
AUTH_SUPER_ADMIN_EMAILS |
["yunchan@twentyoz.kr","hoonjungkoo@hs.ac.kr"] |
학습자·교수자·관리자 공간 접근과 승인 상태를 부여할 슈퍼 관리자 이메일 |
AUTH_APPROVED_EMAILS |
[] |
신규 외부 로그인 시 pending 없이 바로 승인할 이메일 allowlist |
AUTH_NEW_USER_DEFAULT_STATUS |
pending |
Google/SAML 신규 사용자의 기본 승인 상태. dev: 로그인은 로컬/E2E 편의를 위해 자동 승인 |
AUTH_EMAIL_COHORT_MAP |
{} |
특정 이메일을 cohort id로 매핑한다. 값은 comma-separated 문자열도 허용 |
AUTH_DOMAIN_COHORT_MAP |
{} |
이메일/Google hosted domain을 cohort id로 매핑한다. Google/SAML/dev-login 세션 cohort_ids에 반영 |
VIGNETTE_VOICE_POC_SAMPLE_TTS |
false |
P1 무참조 샘플 음성을 /voice/ws TTS에 연결하는 개발 전용 플래그. 마이크/STT는 OPENAI_API_KEY 필요, 프로덕션 금지 |
참고: 프로세스 환경변수(
$env:KEY)는.env보다 우선한다. 일회성 오버라이드에 쓸 수 있다.
2.2 실행
cd apps\api
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
기동 로그에 Application startup complete.가 뜨면 정상이다.
프록시(아래 웹)는 /api 프리픽스를 제거해 이 8000 포트로 붙는다.
2.3 seed 페르소나로 띄우기 (선택)
기본값은 seed 미적재(AUTO_SEED_PERSONAS=false)다. 시스템 페르소나(P1P3)와 저장소 페르소나(P4P7)를 DB 카탈로그에 올려서
바로 회기를 만들고 싶으면 실행 전에 두 플래그를 켠다.
cd apps\api
$env:AUTO_SEED_PERSONAS = "true"
$env:ALLOW_SEED_PERSONA_FALLBACK = "true"
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
main.py lifespan은 settings.auto_seed_personas가 켜져 있을 때만 materialize_seed_personas()를 호출한다.
이 호출은 (code, version) 누락분만 insert하며, 교수자가 수정한 DB 저작본이나 archived 보관본을 덮어쓰거나 재노출하지 않는다.
(또는 apps/api/.env에 두 키를 true로 적어도 된다.)
2.4 DB 없이 degraded 기동 (정상 동작)
DB 연결이 안 되어도 dev에서는 그대로 기동한다. main.py lifespan이 풀 초기화 예외를 잡고
store 인메모리 폴백으로 degraded 기동하며, 다음 경고를 남긴다.
DB 풀 초기화 실패 — store 인메모리 폴백으로 degraded 기동: ...
이때 /health는 다음과 같이 응답한다(DB·엔진 미가용이면 status: degraded).
{ "status": "degraded", "db": false, "engine": false, "environment": "dev", "engine_mode": "claude_cli", ... }
중요: dev에서
db: false는 버그가 아니라 의도된 폴백이다. 실제 DB가 필요하면 Postgres16+pgvector를DATABASE_URL이 가리키는 곳(로컬 dev는127.0.0.1:55432)에 띄우면 된다.ENVIRONMENT가 dev가 아니면 폴백하지 않고 그대로 실패한다.
헬스 체크:
Invoke-RestMethod http://127.0.0.1:8000/health
# 또는
curl.exe http://127.0.0.1:8000/health
3. dev 로그인 (로컬/E2E용 서버 로그인)
프로덕션 경로는 Google OIDC지만, 로컬에서는 dev-login으로 쿠키 세션을 바로 발급받을 수 있다.
조건: ENVIRONMENT=dev 그리고 AUTH_DEV_LOGIN_ENABLED=true, 그리고 요청 Origin/Host가 로컬이어야 한다
(routes/auth.py의 _dev_login_available). 충족 못 하면 404 dev login is disabled.
- 엔드포인트:
POST /auth/dev-login - 본문:
{ "email": ..., "role": "learner|teacher|admin", "display_name": ... } role기본값은learner.- 성공 시
__Host-vignette_sid쿠키(및 dev 전용vignette_sid쿠키)를 세팅한다. - dev-login 사용자는 로컬/E2E 흐름 유지를 위해
account_status=approved로 생성된다. - Google/SAML 신규 사용자는 기본적으로
account_status=pending이며, 승인 전에는/pending화면만 볼 수 있다. 관리자 콘솔/admin/users의 가입 승인 탭에서approved로 바꾸면 역할 홈에 접근한다. - 신규 사용자 또는 온보딩 미완료 사용자는 로그인 직후
/onboarding에서 닉네임, 자기소개, 선택 아바타 이미지, 이름, 소속, 학과, 학년/직위, 전화번호, 주소/수령지와 약관·개인정보 동의를 저장해야 역할 홈으로 이동한다. 학습자 회기 시작은 온보딩 완료와 동의가 모두 있어야 한다. - 프로필 아바타 업로드는 API 작업 디렉터리 기준
USER_UPLOAD_DIR(기본uploads) 아래profile-avatars/에 저장되고,/uploads/profile-avatars/...URL로 서빙된다.
주의(이메일 도메인): dev-login도 기본적으로
validate_google_identity_domain을 거치므로 이메일 도메인이AUTH_ALLOWED_EMAIL_DOMAINS에 있어야 한다. 예:learner@hs.ac.kr. 단, 관리자가/admin/users에 미리 등록한 정확한 이메일은 도메인 밖이어도 로그인할 수 있다. 미등록 외부 도메인은 계속403 email domain is not allowed.
3.1 PowerShell(권장) — Invoke-RestMethod + 세션 쿠키
PowerShell의 curl은 Invoke-WebRequest 별칭이라 JSON 본문·쿠키 다루기가 번거롭다.
PowerShell에서는 Invoke-RestMethod가 가장 깔끔하다.
$body = '{"email":"learner@hs.ac.kr","role":"learner","display_name":"테스트 학습자"}'
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8000/auth/dev-login `
-ContentType 'application/json' -Body $body -SessionVariable s
# 발급된 쿠키로 현재 사용자 확인
Invoke-RestMethod -Uri http://127.0.0.1:8000/auth/me -WebSession $s
3.2 curl.exe(셸 무관) — JSON 본문은 파일로
Windows에서 따옴표 이스케이프 사고를 피하려면 본문을 파일에 넣고 --data-binary @file로 보낸다.
(PowerShell에서는 반드시 curl.exe라고 적어 별칭이 아닌 실제 curl을 호출한다.)
# body.json 작성
'{"email":"learner@hs.ac.kr","role":"learner","display_name":"테스트 학습자"}' |
Out-File -Encoding ascii body.json
# 쿠키 저장(-c) + 본문은 파일(@)로
curl.exe -i -X POST http://127.0.0.1:8000/auth/dev-login `
-H "Content-Type: application/json" --data-binary "@body.json" -c cookies.txt
# 저장한 쿠키(-b)로 재호출
curl.exe http://127.0.0.1:8000/auth/me -b cookies.txt
3.3 웹 UI
apps/web의 로그인 화면(Login.tsx)에 dev-login 경로가 있다. 웹을 띄운 상태(5173)에서
프록시를 통해 /api/auth/dev-login으로 동일하게 동작한다.
로컬/Tailnet 테스트는 dev-login을 사용한다. Google OAuth는 공개 도메인
https://vignette.chanpaca.net에서만 실제 계정 흐름으로 검증한다.
3.4 라이브 코칭 source pack RAG 색인
data/kb/live_coaching_workbook_0615.json와 data/kb/live_coaching_sources/*.json는 라이브 코칭의
기본 근거 source pack이다. API가 DB와 연결된 상태라면 관리자 dev-login 쿠키로 같은 자료를 RAG KB에도
증분 색인할 수 있다.
이 디렉터리는 Docker 이미지가 COPY data ./data로 포함하는 런타임 입력이다. 다른 머신에 심기 전에는
source pack 파일이 워크트리에 존재하는지 반드시 확인한다. commit하지 않는 배포 방식이면 같은 경로로 별도
provision해야 하며, 아래 preflight가 없으면 실패하게 둔다.
python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets
$body = '{"email":"admin@hs.ac.kr","role":"admin","display_name":"로컬 관리자"}'
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8000/auth/dev-login `
-ContentType 'application/json' -Body $body -SessionVariable adminSession
Invoke-RestMethod -Method Post `
-Uri http://127.0.0.1:8000/kb/live-coach/source-packs/sync `
-WebSession $adminSession
응답의 skipped_unchanged가 0보다 크면 content_hash가 동일해 재색인을 건너뛴 source pack이 있다는 뜻이다.
임베딩 모델이 없으면 degraded=true로 BM25-only 색인이 되지만, 라이브 코칭 기본 로컬 근거는 계속 동작한다.
4. 웹(프런트엔드) 실행
cd apps\web
npm run dev
- 접속: http://localhost:5173
- 프록시:
apps/web/vite.config.ts가/api→http://127.0.0.1:8000로 보내며/api프리픽스를 제거한다. (changeOrigin: true,ws: true로 쿠키·WebSocket 전달.) 따라서 브라우저의/api/auth/me는 백엔드의/auth/me에 도달한다. - API가 8000에서 떠 있어야 프록시가 의미가 있다(2장 먼저 실행).
기타 웹 스크립트(apps/web):
npm run typecheck # tsc -b
npm run build # tsc -b && vite build
npm run e2e # Playwright (web + api + DB 스택 필요)
5. 엔진 게이트웨이 (AI 턴 생성)
ENGINE_MODE=claude_cli에서는 실제 발화 생성을 engine_gateway가 담당한다.
이 게이트웨이는 로컬 claude -p(Opus 4.8) 상주 프로세스 풀로, 컨테이너 밖(호스트)에서 9099 포트로 실행한다.
회기 1개 = claude -p 프로세스 1개로 컨텍스트·프롬프트 캐시를 재사용한다.
5.1 실행
cd apps\api
python -m uvicorn engine_gateway.gateway:app --host 127.0.0.1 --port 9099
관련 환경변수(gateway.py):
CLAUDE_BIN— claude 실행 파일 경로(기본claude). PATH에 없으면 절대경로 지정.ENGINE_MODEL— 비우면 CLI 기본(Opus 4.8),ENGINE_FALLBACK_MODEL,SESSION_BUDGET_USD등.
5.2 헬스/레디 확인
게이트웨이는 두 가지 점검 엔드포인트를 제공한다.
GET /health— 얕은 프로세스 liveness.{"ok": true, "engine": "claude_p", ...}GET /ready— 실제로 1회 생성을 돌려claude -p인증/동작까지 증명(성공 200, 실패 503). 설치는 됐지만 로그인 안 된 상태를 여기서 잡는다.
Invoke-RestMethod http://127.0.0.1:9099/health
Invoke-RestMethod http://127.0.0.1:9099/ready
API의 /health는 게이트웨이의 /ready를 호출(404면 /health로 폴백)해 engine 필드를 채운다
(engine_client.health_detail). 즉 게이트웨이가 떠 있어도 claude가 인증 안 됐으면 /ready가 503이라
API /health의 engine: false가 된다.
5.3 프로빙 스크립트 (선택)
이미 떠 있는 게이트웨이의 스트리밍 TTFT/지연/세션 재사용을 측정한다.
python scripts\probe-engine-gateway.py --base-url http://127.0.0.1:9099
python scripts\probe-engine-gateway.py --json # 기계 판독용
기본 base-url은 ENGINE_URL 또는 http://127.0.0.1:9099. 이 스크립트는 실제 세션을 만들어 생성 호출을
하므로(예산 소모) 자격증명은 넘기지 않고 이미 떠 있는 게이트웨이에만 붙는다.
게이트웨이가 없을 때: API
/health의engine: false이고 턴 생성은 실패한다. 다만 UI/네비게이션/로그인/페르소나 조회/세션 생성(in-memory)은 정상 동작한다.
6. (선택) Docker Compose 전체 스택
DB까지 포함한 통합 실행은 infra/docker-compose.yml(db pgvector pg16 + api + web + proxy)을 쓴다.
Docker Desktop이 필요하고, infra/.env(템플릿: infra/.env.example)에 POSTGRES_PASSWORD,
APP_DB_PASSWORD, SESSION_SECRET, OAuth/OpenAI 키 등을 채워야 한다.
cd infra
docker compose up -d db # DB만
docker compose up -d # 전체
배포지에 심기 전에는 저장소 루트에서 preflight를 먼저 실행한다. DB 검증을 붙일 때는 owner 계정이 아니라
API가 쓸 app-role DATABASE_URL을 넘긴다. 실패하면 owner-run init/migration을 먼저 처리하고 API를 띄운다.
python scripts\check-deploy-preflight.py --env-file infra\.env
python scripts\check-deploy-preflight.py --env-file infra\.env --database-url $env:DATABASE_URL --require-app-role
엔진 게이트웨이(9099)는 컴포즈 밖 호스트에서 돌리고, api 컨테이너는
ENGINE_URL=http://host.docker.internal:9099로 호출한다(compose 기본값).
RAG 임베딩/리랭커 의존성은 기본 이미지에 설치하지 않는다. 모델까지 포함한 API 이미지를 만들 때만
infra/.env에 INSTALL_RAG=true를 설정한다.
7. 테스트
# 백엔드 (apps/api)
cd apps\api
python -m pytest app/ -q # 현재 178 pass
python -m pytest engine_gateway\ -q # 현재 11 pass
# 웹 (apps/web)
cd apps\web
npm run typecheck
npm run build
npm run e2e # Playwright — web+api+DB 스택 필요
8. 트러블슈팅
-
포트 충돌(8000/5173/9099): 점유 프로세스 확인 후 종료.
Get-NetTCPConnection -LocalPort 8000 | Select-Object OwningProcess Stop-Process -Id <PID> -Force또는 uvicorn
--port를 바꾼다(웹 프록시는 8000 가정이므로 바꾸면vite.config.tstarget도 함께 조정). -
/health의db: false: dev에서는 정상 degraded 폴백(인메모리 store). 실제 DB가 필요하면DATABASE_URL대상(로컬 dev127.0.0.1:55432)에 Postgres16+pgvector를 띄운다.ENVIRONMENT가 dev가 아니면 폴백하지 않고 기동이 실패한다. -
/health의engine: false/ 턴 생성 실패: 게이트웨이(9099)가 안 떠 있거나claudeCLI가 인증 안 됨. 게이트웨이GET /ready의detail을 보고 원인을 확인한다. 게이트웨이를 먼저 실행하라. -
dev-login이 404 (
dev login is disabled):ENVIRONMENT=dev+AUTH_DEV_LOGIN_ENABLED=true인지, 요청이 로컬 Origin/Host인지 확인.apps/api에서 uvicorn을 실행해.env가 로드됐는지도 확인. -
dev-login이 403 (
email domain is not allowed): 이메일 도메인이AUTH_ALLOWED_EMAIL_DOMAINS에 없고,/admin/users에 정확히 등록된 관리 사용자도 아니다.learner@hs.ac.kr같은 허용 도메인을 쓰거나 관리자가 해당 이메일을 먼저 등록한다. -
/auth/me가 401: 쿠키가 전달되지 않음. curl은-c/-b로 쿠키를 저장·재사용하고, PowerShell은-SessionVariable/-WebSession을 쓴다. 브라우저는 프록시(5173) 경유로 호출해야__Host-쿠키가 동일 출처로 붙는다. -
PowerShell에서
curl이 이상하게 동작: PowerShellcurl은Invoke-WebRequest별칭이다. 실제 curl을 쓰려면curl.exe로 호출하고, JSON 본문은--data-binary "@body.json"처럼 파일로 넘긴다. -
한글/공백 경로: 경로에 공백·한글이 있으면 큰따옴표로 감싼다.
.env/JSON 파일은 UTF-8(또는 ascii)로 저장.