533 lines
29 KiB
Markdown
533 lines
29 KiB
Markdown
# 로컬 개발 실행 가이드
|
|
|
|
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
|
|
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:55432` DB 컨테이너를 사용한다. 새 컨테이너 생성 시 `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 정리는 지정한 `-ApiPort` listener만 대상으로 한다. public `8001`, Tailnet `8010`, local `8000`을 나눠 띄운 상태에서 API-only 재기동할 때 다른 포트를 건드리지 않는다.
|
|
- `dev-down.ps1`은 기본적으로 DB 컨테이너를 보존한다. 컨테이너도 멈추려면 `-Db`를 명시한다.
|
|
|
|
> 스크립트는 uvicorn이 설치된 python을 자동 해석한다(시스템에 복수 python 공존 시 'python' 별칭이
|
|
> uvicorn 없는 인터프리터를 가리킬 수 있음 — 이 함정 때문에 명시 해석함).
|
|
|
|
### 0.1 Tailscale PC/모바일 접속
|
|
|
|
같은 Tailnet에 로그인한 장비에서는 아래 명령으로 Tailnet 전용 HTTPS 접속점을 켠다.
|
|
|
|
```powershell
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-tailscale-runtime.ps1
|
|
```
|
|
|
|
- 현재 머신의 접속 주소: **https://alpaca-home.taile93291.ts.net**
|
|
- 이 경로는 `tailscale serve`로 `https://alpaca-home.taile93291.ts.net` → 로컬 web `127.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` 안내로 되돌린다.
|
|
|
|
### 0.2 Public runtime watchdog
|
|
|
|
공개 API 복구 스크립트는 `docs/ops/public-runtime-watchdog.md`가 runbook이다. 로컬 개발 서버와 별개로
|
|
prod API 8001, web preview 5174, engine gateway 9099, cloudflared tunnel을 검사한다.
|
|
|
|
```powershell
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\watch-public-runtime.ps1 -CheckOnly
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-public-runtime-task.ps1 -RunNow
|
|
```
|
|
|
|
- Scheduled Task는 현재 Windows 사용자 기준 `AtLogOn` + 반복 watchdog이다. 사용자 로그인 전 headless boot service가 아니다.
|
|
- secret은 task 인자에 넣지 않는다. API secret은 `apps/api/.env`, cloudflared/Claude CLI credential은 사용자 profile에 둔다.
|
|
- 아직 DNS가 없는 future host는 기본 검사에 넣지 않는다. `api-vnet.18ka.net`처럼 실제로 열린 뒤에만 `-AdditionalPublicHealthUrls`로 명시 추가한다.
|
|
- 완료 판정은 parser/check-only가 아니라 실제 재부팅 또는 로그오프/로그온 뒤 `Get-ScheduledTaskInfo`, watchdog 로그의 `restart verified`, public health, 인증된 public `/turn` smoke까지 한 세트로 남겨야 한다.
|
|
|
|
### 수동 기동(대안)
|
|
|
|
DB·엔진 없이도 UI/로그인/페르소나/세션 생성까지는 동작한다(턴 생성만 불가).
|
|
|
|
1. **API** — `apps/api`에서
|
|
`python -m uvicorn app.main:app --host 127.0.0.1 --port 8000`
|
|
2. **웹** — `apps/web`에서 `npm run dev` → http://localhost:5173
|
|
3. (선택) **엔진 게이트웨이** — `apps/api`에서
|
|
`python -m uvicorn engine_gateway.gateway:app --host 127.0.0.1 --port 9099`
|
|
(실제 AI 턴 생성을 하려면 필요. `claude` CLI 설치+로그인 전제)
|
|
|
|
아래에서 각 단계를 자세히 설명한다.
|
|
|
|
---
|
|
|
|
## 1. 사전 준비
|
|
|
|
- **Python 3.11** (운영 스크립트가 Python 3.11 기준). 가상환경 권장.
|
|
- **Node.js 22 + npm** — CI와 같은 기준. newer LTS는 별도 재검증 전까지 기준선으로 쓰지 않는다.
|
|
- (선택) **Docker Desktop** — `infra/docker-compose.yml` 전체 스택을 띄울 때만.
|
|
- (선택) **`claude` CLI** — `ENGINE_MODE=claude_cli`로 실제 턴 생성을 할 때. 설치 후 로그인되어 있어야 한다.
|
|
|
|
### Python 의존성 설치 (`apps/api`)
|
|
|
|
```powershell
|
|
# 저장소 루트에서
|
|
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`)
|
|
|
|
```powershell
|
|
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` 필요, 프로덕션 금지 |
|
|
| `EVALUATOR_SEMANTIC_CACHE_ENABLED` | `true` | fast/deep evaluator structured 결과 인메모리 캐시 활성화. 원문 prompt/completion은 저장하지 않음 |
|
|
| `EVALUATOR_SEMANTIC_CACHE_TTL_SECONDS` | `900` | evaluator cache TTL(초). 0 이하면 비활성 |
|
|
| `EVALUATOR_SEMANTIC_CACHE_MAX_ENTRIES` | `256` | evaluator cache LRU 최대 엔트리 수. 0 이하면 비활성 |
|
|
| `NOTIFICATION_EMAIL_PROVIDER` | `disabled` | 운영 메일 provider. 실제 발송은 `smtp`와 `SMTP_*` 설정이 있을 때만 수행 |
|
|
| `SMTP_HOST` / `SMTP_FROM_EMAIL` | 빈 값 | `NOTIFICATION_EMAIL_PROVIDER=smtp`일 때 필요한 SMTP 호스트와 발신 주소 |
|
|
|
|
> 참고: 프로세스 환경변수(`$env:KEY`)는 `.env`보다 우선한다. 일회성 오버라이드에 쓸 수 있다.
|
|
|
|
### 2.2 실행
|
|
|
|
```powershell
|
|
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`)다. 시스템 페르소나(P1~P3)와 저장소 페르소나(P4~P7)를 DB 카탈로그에 올려서
|
|
바로 회기를 만들고 싶으면 실행 전에 두 플래그를 켠다.
|
|
|
|
```powershell
|
|
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`로 적어도 된다.)
|
|
|
|
seed manifest만 확인할 때는 API 서버 없이 저장소 루트에서 dry-run runner를 실행한다. Windows에서 `python` 별칭이 다른 인터프리터를 가리키면 API 의존성이 없을 수 있으므로, 이 저장소에서는 Python 3.11 런처를 우선 쓴다. `--apply`는 DB pool을 초기화한 뒤 기존 idempotent DB materializer를 호출하므로 로컬 DB 대상이 맞는지 확인한 뒤에만 쓴다.
|
|
|
|
```powershell
|
|
cd D:\workspace\vignette
|
|
py -3.11 scripts\materialize-persona-seeds.py --json
|
|
```
|
|
|
|
### 2.4 M2 session digest worker 실행
|
|
|
|
세션 종료 시 저장되는 fallback digest를 LLM 후보로 압축해 볼 때는 명시 session id runner를 쓴다.
|
|
기본은 dry-run이며, DB row를 바꾸려면 `--apply`를 반드시 붙인다. runner는 DB에서 작업을 읽은 뒤
|
|
connection을 놓고 engine을 호출하고, accepted 결과만 짧은 DB acquire로 적용한다.
|
|
|
|
```powershell
|
|
cd D:\workspace\vignette
|
|
py -3.11 scripts\run-session-digest-worker.py --session-id <session_uuid> --json
|
|
|
|
# accepted 후보를 실제 session_summary/case_profile에 반영할 때만
|
|
py -3.11 scripts\run-session-digest-worker.py --session-id <session_uuid> --apply --json
|
|
```
|
|
|
|
- 출력은 기본적으로 metadata-only다. digest 본문은 민감할 수 있으므로 `--show-digest`를 명시할 때만 출력한다.
|
|
- 입력은 client-visible `text_masked` transcript와 open thread만 사용한다. raw `text`, evaluator-only turn, CCD, `end_state`는 압축 prompt에 넣지 않는다.
|
|
- API 서버는 `SESSION_DIGEST_WORKER_ENABLED=true`일 때만 세션 종료 뒤 같은 worker를 background task로 실행한다. 기본값은 false다.
|
|
- scheduler도 DB load/apply 구간만 connection을 잡고, engine 호출은 DB transaction 밖에서 수행한다.
|
|
- 장시간 provider 운영, 임상 골든셋 품질평가, 재압축 정책은 별도 gate다.
|
|
|
|
### 2.5 DB 없이 degraded 기동 (정상 동작)
|
|
|
|
DB 연결이 안 되어도 dev에서는 그대로 기동한다. `main.py` lifespan이 풀 초기화 예외를 잡고
|
|
`store` 인메모리 폴백으로 degraded 기동하며, 다음 경고를 남긴다.
|
|
|
|
```
|
|
DB 풀 초기화 실패 — store 인메모리 폴백으로 degraded 기동: ...
|
|
```
|
|
|
|
이때 `/health`는 다음과 같이 응답한다(DB·엔진 미가용이면 `status: degraded`).
|
|
|
|
```json
|
|
{ "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가 아니면
|
|
> 폴백하지 않고 그대로 실패한다.
|
|
|
|
헬스 체크:
|
|
|
|
```powershell
|
|
Invoke-RestMethod http://127.0.0.1:8000/health
|
|
# 또는
|
|
curl.exe http://127.0.0.1:8000/health
|
|
```
|
|
|
|
운영 콘솔 헬스 샘플을 브라우저 방문 없이 DB에 1회 적재하려면 저장소 루트에서 아래를 실행한다.
|
|
이 스크립트는 `apps/api/.env`를 읽고 API lifespan과 같은 DB/engine/voice 초기화 경로를 사용해
|
|
`app.admin_health_event`에 서비스별 샘플을 append한다. SLA 수치가 아니라 관측 샘플 이력이다.
|
|
|
|
```powershell
|
|
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\record-admin-health-sample.py --json
|
|
|
|
# 예약 작업 명령 확인만
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-health-sampler-task.ps1 -PrintOnly
|
|
|
|
# 실제 등록 + 즉시 1회 실행
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-health-sampler-task.ps1 -IntervalMinutes 5 -RunNow
|
|
```
|
|
|
|
쌓인 원시 헬스 샘플은 명시 기간을 준 maintenance 스크립트로 일별 rollup에 먼저 집계한 뒤 정리한다.
|
|
기본은 dry-run이며, 실제 삭제는 `--apply`를 붙였을 때만 수행한다. non-dev 환경에서는
|
|
`--allow-non-dev-apply`가 없으면 apply가 차단된다.
|
|
|
|
```powershell
|
|
# 영향 범위 확인
|
|
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\maintain-admin-health-events.py --rollup-days 2 --retention-days 30 --json
|
|
|
|
# dev DB에서만 실제 적용
|
|
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\maintain-admin-health-events.py --rollup-days 2 --retention-days 30 --apply --json
|
|
```
|
|
|
|
---
|
|
|
|
## 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`로 바꾸면 역할 홈에 접근한다.
|
|
DB가 연결되어 있으면 pending 생성 시 `app.notification_event`/`app.notification_delivery`에 가입 승인
|
|
메일 큐가 생긴다. `NOTIFICATION_EMAIL_PROVIDER=disabled`인 로컬 기본값에서는 실제 메일은 발송하지 않는다.
|
|
- 신규 사용자 또는 온보딩 미완료 사용자는 로그인 직후 `/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 메일 알림 큐 확인/처리
|
|
|
|
가입 승인과 교수자 회기 검토 알림은 상태 원본이 아니라 보조 알림이다. 원본 상태는 각각
|
|
`app.app_user.account_status`, `app.session_review_status`이고, 메일 전송 상태만
|
|
`app.notification_delivery`에 남는다.
|
|
|
|
```powershell
|
|
cd D:\workspace\vignette
|
|
# SMTP 설정이 준비된 환경에서 큐를 한 번 처리
|
|
python scripts\run-notification-worker.py --limit 25
|
|
```
|
|
|
|
관리자 API에서도 `GET /admin/notifications`로 최근 delivery를 보고,
|
|
`POST /admin/notifications/process`로 한 번 처리할 수 있다. `POST /admin/notifications/test`는
|
|
`AUTH_SUPER_ADMIN_EMAILS` 대상에게 테스트 메일 이벤트를 만들고 같은 큐로 즉시 처리한다. 메일 본문에는 축어록이나 평가 전문을 넣지 않고,
|
|
`/admin/users`, `/teach/session/:sessionId/review`, `/admin` 링크만 제공한다.
|
|
|
|
### 3.2 PowerShell(권장) — Invoke-RestMethod + 세션 쿠키
|
|
|
|
PowerShell의 `curl`은 `Invoke-WebRequest` 별칭이라 JSON 본문·쿠키 다루기가 번거롭다.
|
|
PowerShell에서는 `Invoke-RestMethod`가 가장 깔끔하다.
|
|
|
|
```powershell
|
|
$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.3 curl.exe(셸 무관) — JSON 본문은 파일로
|
|
|
|
Windows에서 따옴표 이스케이프 사고를 피하려면 본문을 파일에 넣고 `--data-binary @file`로 보낸다.
|
|
(PowerShell에서는 반드시 `curl.exe`라고 적어 별칭이 아닌 실제 curl을 호출한다.)
|
|
|
|
```powershell
|
|
# 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.4 웹 UI
|
|
|
|
`apps/web`의 로그인 화면(`Login.tsx`)에 dev-login 경로가 있다. 웹을 띄운 상태(5173)에서
|
|
프록시를 통해 `/api/auth/dev-login`으로 동일하게 동작한다.
|
|
|
|
> 로컬/Tailnet 테스트는 dev-login을 사용한다. Google OAuth는 공개 도메인
|
|
> `https://vignette.chanpaca.net`에서만 실제 계정 흐름으로 검증한다.
|
|
|
|
### 3.5 라이브 코칭 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가 없으면 실패하게 둔다.
|
|
|
|
```powershell
|
|
python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets
|
|
```
|
|
|
|
```powershell
|
|
$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이 있다는 뜻이다.
|
|
현재 sync 경로는 active `kb.document`의 hash/version을 먼저 읽고, hash가 바뀐 pack만 다음 document version으로 색인한다.
|
|
임베딩 모델이 없으면 `degraded=true`로 BM25-only 색인이 되지만, 라이브 코칭 기본 로컬 근거는 계속 동작한다.
|
|
|
|
API 서버 없이 DB에 직접 비교/적용하려면 저장소 루트에서 runner를 쓴다. 기본은 DB-backed dry-run이라 write하지 않고, `--apply`에서만 `kb.source` upsert와 `kb.document/kb.chunk` 색인을 수행한다.
|
|
|
|
```powershell
|
|
cd D:\workspace\vignette
|
|
py -3.11 scripts\sync-persona-sources.py --json
|
|
py -3.11 scripts\sync-persona-sources.py --apply --json
|
|
```
|
|
|
|
DB 없이 CLI shape만 확인하려면:
|
|
|
|
```powershell
|
|
py -3.11 scripts\sync-persona-sources.py --help
|
|
```
|
|
|
|
---
|
|
|
|
## 4. 웹(프런트엔드) 실행
|
|
|
|
```powershell
|
|
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`):
|
|
|
|
```powershell
|
|
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 실행
|
|
|
|
```powershell
|
|
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).
|
|
설치는 됐지만 로그인 안 된 상태를 여기서 잡는다.
|
|
|
|
```powershell
|
|
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/지연/세션 재사용을 측정한다.
|
|
|
|
```powershell
|
|
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 키 등을 채워야 한다.
|
|
|
|
```powershell
|
|
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를 띄운다.
|
|
|
|
```powershell
|
|
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. 테스트
|
|
|
|
```powershell
|
|
# 백엔드 (apps/api)
|
|
cd apps\api
|
|
python -m pytest app/ -q # 백엔드 기준선 178 pass
|
|
python -m pytest engine_gateway\ -q # 현재 27 pass
|
|
|
|
# 웹 (apps/web)
|
|
cd apps\web
|
|
npm run typecheck
|
|
npm run build
|
|
npm run e2e # Playwright — web+api+DB 스택 필요
|
|
```
|
|
|
|
---
|
|
|
|
## 8. 트러블슈팅
|
|
|
|
- **포트 충돌(8000/5173/9099)**: 점유 프로세스 확인 후 종료.
|
|
```powershell
|
|
Get-NetTCPConnection -LocalPort 8000 | Select-Object OwningProcess
|
|
Stop-Process -Id <PID> -Force
|
|
```
|
|
또는 uvicorn `--port`를 바꾼다(웹 프록시는 8000 가정이므로 바꾸면 `vite.config.ts` target도 함께 조정).
|
|
|
|
- **`/health`의 `db: false`**: dev에서는 정상 degraded 폴백(인메모리 store). 실제 DB가 필요하면
|
|
`DATABASE_URL` 대상(로컬 dev `127.0.0.1:55432`)에 Postgres16+pgvector를 띄운다. `ENVIRONMENT`가 dev가
|
|
아니면 폴백하지 않고 기동이 실패한다.
|
|
|
|
- **`/health`의 `engine: false` / 턴 생성 실패**: 게이트웨이(9099)가 안 떠 있거나 `claude` CLI가
|
|
인증 안 됨. 게이트웨이 `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`이 이상하게 동작**: PowerShell `curl`은 `Invoke-WebRequest` 별칭이다.
|
|
실제 curl을 쓰려면 `curl.exe`로 호출하고, JSON 본문은 `--data-binary "@body.json"`처럼 파일로 넘긴다.
|
|
|
|
- **한글/공백 경로**: 경로에 공백·한글이 있으면 큰따옴표로 감싼다. `.env`/JSON 파일은 UTF-8(또는 ascii)로 저장.
|