외부 계정 수동 등록 허용
This commit is contained in:
parent
bd389a97cc
commit
8ed185ce6c
9 changed files with 3450 additions and 1484 deletions
|
|
@ -36,7 +36,7 @@ vignette/
|
|||
│ │ ├─ app/ 라우터·서비스·스토어·DB·인증
|
||||
│ │ └─ engine_gateway/ 별도 서비스: claude -p 상주 풀 / provider 어댑터
|
||||
│ └─ web/ React 19 + Vite + Playwright 프론트
|
||||
├─ infra/ docker-compose(db+api), DB 초기화 SQL
|
||||
├─ infra/ docker-compose(db+api+web+proxy), DB 초기화 SQL
|
||||
├─ docs/ 설계·운영 문서 (이 파일 포함)
|
||||
└─ scripts/ 운영 스크립트(PowerShell 등)
|
||||
```
|
||||
|
|
@ -80,7 +80,7 @@ vignette/
|
|||
`lifespan`(startup/shutdown)에서:
|
||||
|
||||
1. `init_pool()` → DB 풀 생성, `ensure_runtime_tables()` / `ensure_review_tables()` 보장.
|
||||
2. `settings.auto_seed_personas` 면 `materialize_seed_personas()`로 시드 P1~P3과 저장소 `data/personas/P4~P7.json`을 `app.persona_card`에 upsert.
|
||||
2. `settings.auto_seed_personas` 면 `materialize_seed_personas()`로 시스템 페르소나 P1~P3과 저장소 `data/personas/P4~P7.json`을 `app.persona_card`에 누락분만 물리화한다. 기존 DB 저작본/보관본은 덮어쓰거나 되살리지 않는다.
|
||||
3. `engine_client.startup()` / `voice_service.startup()`로 httpx 클라이언트 준비.
|
||||
4. **DB 초기화 실패 시** `environment == "dev"`이면 예외를 삼키고 경고만 남긴 채 degraded 기동한다
|
||||
(그 외 환경은 raise). `app/main.py:47-53`.
|
||||
|
|
@ -150,7 +150,8 @@ assistant/user 히스토리(L6) → `user(이번 마스킹 발화, L5)`. 게이
|
|||
- 자살·자해의 구체적 '방법/수단'은 절대 발화하지 않는다.
|
||||
- `_format_openness_directive()`가 `effective_openness` 수치를 연기 강도 지시문으로 환산한다(수치 자체는 비노출).
|
||||
|
||||
시드 페르소나(개발 부트스트랩): `SEED_PERSONAS = {P1, P2, P3}`와 저장소 `data/personas/P4~P7.json` — `persona_repository.built_in_personas()`.
|
||||
시스템 페르소나 부트스트랩: `SEED_PERSONAS = {P1, P2, P3}`와 저장소 `data/personas/P4~P7.json` — `persona_repository.built_in_personas()`.
|
||||
초기 DB 카탈로그를 채우기 위한 원천일 뿐, 승인 이후 편집/보관 결정은 `app.persona_card`가 SSOT다.
|
||||
|
||||
| 코드 | 인물 | 난이도 | 이론타깃 | 특징 |
|
||||
|---|---|---|---|---|
|
||||
|
|
@ -222,6 +223,27 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
|
|||
⚠️ 의존 방향: evaluator는 `taxonomy/engine_client/orchestrator/state_machine/persona`를 *읽기 전용*으로만
|
||||
의존하고, orchestrator는 evaluator를 import 하지 않는다(단방향).
|
||||
|
||||
### 2.6.1 라이브 코칭 AI — `app/services/live_coach.py`
|
||||
|
||||
라이브 코칭은 내담자 생성 루프에 끼워 넣지 않는 별도 슈퍼비전 경로다. `POST /sessions/{id}/stream`
|
||||
또는 음성 턴이 끝난 뒤, 프론트가 `POST /sessions/{id}/live-coach`를 호출해 "다음 한 문장" 중심의
|
||||
짧은 코칭을 받는다. 엔진/RAG 장애는 상담 흐름을 막지 않고 워크북 기반 규칙 코칭으로 degrade한다.
|
||||
|
||||
- 입력은 `guardrail.mask_pii()` 후 evaluator 역할로 전송한다. 페르소나 CCD·상태 수치·정답키는 학습자에게
|
||||
노출하지 않는다.
|
||||
- 기본 근거는 `data/kb/live_coaching_workbook_0615.json`와 `data/kb/live_coaching_sources/*.json`의
|
||||
허가된 0615 워크북·DSM·공식 지침 요약 청크다. 추가 RAG는 evaluator view로
|
||||
`theory/technique/supervisor_pattern/microskill/taxonomy` 근거만 회수한다.
|
||||
- 관리자 `POST /kb/live-coach/source-packs/sync`는 같은 source pack을 `kb.source`와 `kb.chunk`에
|
||||
content_hash 기반으로 증분 색인한다. 기본 visibility는 evaluator 전용이고, C/D·diagnostic 자료는
|
||||
sensitivity 2로 고정한다.
|
||||
- 출력은 `LiveCoachSuggestion` 구조화 JSON(`tone/focus/title/message/next_utterance/rationale/sources`).
|
||||
UI는 코칭 아바타 말풍선, 근거 모달, 발화별 코칭 이력 오버레이로 표시한다.
|
||||
- 전달된 코칭은 `app.live_coach_events`에 저장된다. 원문 축어록을 중복 저장하지 않고 PII 마스킹된 짧은
|
||||
learner/client excerpt와 코칭 payload만 저장한다. DB 미가용 dev에서는 런타임 캐시로 폴백한다.
|
||||
- DSM/공식 지침/논문은 사용 허가된 source pack으로 투입할 수 있다. 다만 코칭 프롬프트와 UI에는
|
||||
장문 원문이나 공식 문항을 재현하지 않고, chunk summary + version/citation + 출처 식별자로 노출한다.
|
||||
|
||||
### 2.7 회기 메모리 — `app/services/memory.py`
|
||||
|
||||
회기 라이프사이클 메모리(4계층 매핑: ① working / ② episodic / ③ summary / ④ semantic).
|
||||
|
|
@ -240,9 +262,13 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
|
|||
OpenAI STT/TTS 어댑터(순수 변환 + voice preset 매핑). 상담 로직은 라우트가 조립한다.
|
||||
|
||||
- STT: `/audio/transcriptions` (gpt-4o-transcribe → 404 시 whisper-1 폴백), 언어 힌트 `ko`.
|
||||
- TTS: `/audio/speech` (gpt-4o-mini-tts → tts-1 폴백). 페르소나 preset → OpenAI voice 매핑
|
||||
(`PRESET_TO_OPENAI_VOICE`, P1=coral / P2=ash / P3=shimmer). 스트림 청크마다 립싱크 RMS 힌트
|
||||
(`estimate_chunk_rms`, PCM 디코딩 없이 바이트 에너지 근사) 동봉.
|
||||
- TTS: `/audio/speech` (gpt-4o-mini-tts → tts-1 폴백). 음성 선택 우선순위는 명시 query preset →
|
||||
`app.persona_voice_map`의 OpenAI row(`base_params.preset/openai_voice/rate/instructions`) →
|
||||
페르소나 code 기본 preset(`PRESET_TO_OPENAI_VOICE`, P1=coral / P2=ash / P3=shimmer)이다.
|
||||
OpenAI가 아닌 provider row는 현재 live OpenAI TTS로 보내지 않고 기존 fallback을 사용한다.
|
||||
dev 런타임 스키마 보강은 기존 DB의 `app.persona_voice_map` 누락도 복구해 seed materializer와
|
||||
`/voice/ws` 바인딩이 같은 테이블을 사용하게 한다.
|
||||
스트림 청크마다 립싱크 RMS 힌트(`estimate_chunk_rms`, PCM 디코딩 없이 바이트 에너지 근사)를 동봉한다.
|
||||
- 키 없으면 명확히 degraded(`is_available()=False`, `VoiceUnavailable`). 라우트가 503/WS close로 변환.
|
||||
|
||||
`/voice/ws` WebSocket 캐스케이드(`routes/voice.py`):
|
||||
|
|
@ -272,19 +298,58 @@ server: ready → state(listening) → state(thinking) → transcript → reply
|
|||
순차 append → 상태 갱신. `EngineError`는 503으로 변환.
|
||||
- `POST /sessions/{id}/stream` — SSE 경로(`run_turn_stream`). token/done/ping/error를 흘리고,
|
||||
done 시점에 학습자 발화 + 누적 내담자 응답을 영속화. `sse_heartbeat_seconds`마다 ping(Cloudflare 타임아웃 회피).
|
||||
- `POST /sessions/{id}/live-coach` — 방금 완료된 상담자 발화를 워크북 요약/RAG/evaluator fast-loop 신호와 대조해
|
||||
라이브 코칭 카드 1개를 반환하고 `app.live_coach_events`에 저장한다. 저장 payload는 마스킹 excerpt와 코칭 구조화 JSON이다.
|
||||
- `GET /sessions/{id}/live-coach` — 현재 회기에서 학습자에게 실제 전달된 코칭 이력을 시간순으로 반환한다.
|
||||
세션 화면은 이 응답을 `turn_seq`별로 묶어 학습자 발화 우측 코칭 마커와 채팅 위 스크롤 오버레이에 표시한다.
|
||||
- `POST /kb/live-coach/source-packs/sync` — 관리자 전용. 로컬 라이브 코칭 source pack을 `kb.source` upsert 후
|
||||
`kb.document/kb.chunk`로 색인한다. 임베딩 모델이 없으면 BM25-only degraded 색인으로 진행한다.
|
||||
- `POST /sessions/{id}/end` — `memory.make_carry_over(...)` → 세션 종료 + carry 준비 →
|
||||
`_schedule_session_evaluation(sess)`로 deep-loop 평가를 비동기 태스크로 발사.
|
||||
- `GET /sessions/dashboard` — 학습자 본인 세션만 `include_turn_evaluation=true`로 집계해
|
||||
`overview/growth/persona_progress/achievements/recent_feedback`를 반환한다. `overview.archived_sessions`는
|
||||
학습자별 보관 상태(`app.session_archive_state`) 기준 카운트이며, 보관된 종료 회기는 리뷰 대기 행동 큐에서
|
||||
제외한다. 성취는 공식 등급/수료가 아니라 실제 연습 milestone만 표시한다.
|
||||
- `GET /sessions/{id}/review` — 저장된 축어록 + 평가 AI 산출물로 학습자-안전 리뷰 구성
|
||||
(`_rubric_from_evaluation`, `_ai_review_points` 등). 리뷰 조회 시 발화별 fast-loop 평가는
|
||||
`app.feedback_scores`/라벨 조인 테이블에서 `TurnRecord.evaluation` 형태로 hydrate한다.
|
||||
평가 미완이면 `degraded/reviewReady=false`로 표기. 학습자가 저장한 사례개념화 워크시트가 있으면
|
||||
자동 초안보다 `saved_by_learner` 저장본을 우선 반환한다.
|
||||
자동 초안보다 `saved_by_learner` 저장본을 우선 반환한다. 교수자/관리자는 담당 범위 회기를 읽기
|
||||
전용으로 검토할 수 있지만, 학습자 워크시트 저장 권한은 learner 전용으로 유지한다.
|
||||
`ReviewNote.body`는 제한 markdown(`code`, `strong`, blockquote, list)으로 렌더링하고,
|
||||
`effective_openness` 같은 내부 수치는 `effective openness(유효 개방도)`로 학술 용어화한다.
|
||||
상담자 질문/발화 근거는 `ReviewNote.quote`로 분리해 리뷰 카드 안에서 인용 블록으로 표시한다.
|
||||
- `POST /sessions/{id}/share` — 학습자가 종료된 본인 회기 리뷰를 URL로 공유하기 위해 공개 토큰을 생성한다.
|
||||
서버는 `app.session_share_link`에 토큰 해시와 sanitized preview payload만 저장하고, 원문 축어록·학습자
|
||||
식별자는 payload에 넣지 않는다. 토큰은 생성 응답에서만 반환되며 새 생성은 기존 토큰을 교체한다.
|
||||
- `DELETE /sessions/{id}/share` — 해당 회기의 공개 공유 토큰을 폐기한다.
|
||||
- `POST /sessions/{id}/archive` / `POST /sessions/{id}/restore` — 학습자 본인의 종료 회기를 보관/복원한다.
|
||||
진행 중 회기는 409로 거부한다. 보관은 `app.session_archive_state`만 upsert/delete하는 보기 상태이며,
|
||||
`app.sessions`, `app.turns`, 리뷰, 공유 링크, 연구/감사 증거는 삭제하지 않는다.
|
||||
- `PUT /sessions/{id}/review/worksheet` — 학습자가 수정한 사례개념화 워크시트를 저장한다.
|
||||
저장본은 `app.case_worksheet`에 session 단위로 upsert되며, owner learner 또는 teacher/admin RLS 범위에서만 읽힌다.
|
||||
|
||||
발화 가시성: 학습자에게는 `visible_to`에 `counselor`가 포함된 턴만 보여준다
|
||||
(`_learner_visible_turns` → `_LEARNER_VISIBLE_AI_ROLE="counselor"`). 평가 전용 데이터는 노출되지 않는다.
|
||||
|
||||
### 2.9.1 교수자 회기 검토 상태 — `app/routes/teacher.py`
|
||||
|
||||
- `GET /teacher/dashboard` — 담당 학습자 성장, 안전 알림, 종료 회기 검토 큐를 반환한다. 회기 요약에는
|
||||
`review_status`, `review_note`, `reviewed_at`를 포함해 교수자가 이미 검토한 회기를 구분한다.
|
||||
- `PUT /teacher/sessions/{session_id}/review-status` — 교수자/관리자가 회기 검토 메모와 상태를 저장한다.
|
||||
저장 대상은 `app.session_review_status`이며, 검토 완료된 회기는 pending queue에서 제외된다.
|
||||
- `/teach/session/:sessionId/review` 화면은 같은 `GET /sessions/{id}/review` 자료를 교수자 읽기 전용으로 표시하고,
|
||||
검토 메모 저장은 위 teacher endpoint로 분리한다.
|
||||
|
||||
### 2.9.2 공개 공유·검색 메타 — `app/routes/share.py`
|
||||
|
||||
- `GET /share/session/{token}` — 인증 없이 접근 가능한 unfurl HTML. Open Graph/Twitter Card/JSON-LD를 서버에서
|
||||
직접 내려 URL만 전달해도 카카오톡·Slack·메일·AI 브라우저가 제목/요약/썸네일을 읽을 수 있게 한다.
|
||||
- `GET /share/session/{token}/summary` — 동일 sanitized payload의 JSON 응답. 회기 원문, 턴별 축어록, 학습자
|
||||
식별 정보는 포함하지 않는다.
|
||||
- 공유 HTML과 API 도메인 `/robots.txt`는 `noindex`/`Disallow: /share/` 정책을 둔다. 공유 URL은 검색 색인용
|
||||
공개 문서가 아니라 교수자 전달용 미리보기 카드다.
|
||||
|
||||
### 2.10 엔진 클라이언트 — `app/engine_client.py`
|
||||
|
||||
게이트웨이 HTTP 클라이언트(호출부만). 계약:
|
||||
|
|
@ -318,10 +383,18 @@ GET {ENGINE_URL}/ready|/health — readiness/liveness
|
|||
DB `app.persona_card`가 승인 페르소나의 SoR. 이 모듈이 in-proc `PersonaCard`와 DB 행을 잇는 경계다.
|
||||
|
||||
- `load_file_personas()` / `built_in_personas()` — in-code P1~P3과 저장소 `data/personas/P4~P7.json`을 deterministic catalog로 합친다.
|
||||
- `materialize_seed_personas()` — built-in P1~P7을 `status='approved'`로 upsert(admin 롤).
|
||||
- `materialize_seed_personas()` — built-in P1~P7을 초기 승인 카탈로그로 누락분만 insert(admin 롤). `ON CONFLICT DO NOTHING`이므로 교수 편집본을 덮어쓰거나 `archived` 보관본을 재승인하지 않는다.
|
||||
- `create_persona_revision_from_existing()` — 공개 승인본을 같은 `persona_id`의 다음 `version` draft/review로 복제한다. 이미 열린 draft/review가 있으면 새 버전을 만들지 않고 기존 초안을 돌려준다.
|
||||
- `archive_persona_family()` — 교수/관리자 UI의 삭제 동작. 세션 FK 보존을 위해 hard delete 대신 같은 `code` family의 비보관 버전을 모두 `status='archived'`로 전환한다.
|
||||
- `get_approved_persona(code)` / `list_approved_personas()` — 승인된 최신 버전 조회(AI 컨텍스트).
|
||||
- `get_catalog_persona(code)` — 승인 카드 조회, 실패 시 `settings.allow_seed_persona_fallback`이면
|
||||
`seed_fallback_persona`(degraded=True)로 폴백.
|
||||
- 페르소나 스튜디오: `/teach/personas`가 teacher/admin 전용 저작 작업면이다. 교수 콘솔은 진입점/triage만 맡고,
|
||||
실제 저작은 개요·임상·저항·회기·말투·안전·프롬프트 탭으로 분리한다.
|
||||
- RAG 첨부 SSOT: `POST /personas/sources`는 첨부/붙여넣기 자료를 `mask_pii()` 후
|
||||
`kb.source/document/chunk`에 evaluator 전용 근거(`visible_to=['evaluator']`, `sensitivity=2`)로 등록한다.
|
||||
`POST /personas/drafts/generate`는 raw text가 아니라 `source_id` 기반 RAG 검색 결과를 생성 근거로 사용하고,
|
||||
draft `source_provenance`에 source id와 chunk id를 남긴다.
|
||||
- 교수 검수: `list_persona_review_queue(role)` / `update_persona_review_status(action=approve|reject)`
|
||||
— 승인/반려 시 `audit.audit_log`에 감사 기록.
|
||||
|
||||
|
|
@ -367,12 +440,20 @@ React 19 + Vite. 라우팅은 `apps/web/src/App.tsx`(react-router-dom).
|
|||
| 경로 | 화면 | 역할 가드 |
|
||||
|---|---|---|
|
||||
| `/login` | Login (dev-login 경로 포함) | 공개 |
|
||||
| `/learn` | LearnerHome | learner |
|
||||
| `/pending` | PendingApproval(승인 대기/보류 안내) | 인증됨, approved 전용 제한 화면 |
|
||||
| `/learn` | LearnerHome(대시보드: 학습 요약, 최근 회기 리캡, AI 코치) | learner |
|
||||
| `/learn/practice` | LearnerHome(연습 대상 선택·새 회기 시작) | learner |
|
||||
| `/learn/history` | LearnerHome(회기 기록·보관/복원·리뷰 진입) | learner |
|
||||
| `/learn/session/:sessionId` | Session(상담 화면) | learner |
|
||||
| `/learn/session/:sessionId/review` | SessionReview(회기 리뷰) | learner |
|
||||
| `/learn/avatar-expressions` | AvatarExpressionLab | learner |
|
||||
| `/teach` | Professor(교수자 대시보드) | teacher |
|
||||
| `/admin` | Admin | admin |
|
||||
| `/teach/personas` | PersonaStudio(페르소나 저작·검수) | teacher/admin |
|
||||
| `/teach/session/:sessionId/review` | SessionReview(교수자 읽기 전용 회기 검토) | teacher |
|
||||
| `/admin` | Admin(운영 홈) | admin |
|
||||
| `/admin/users` | Admin(사용자 관리) | admin |
|
||||
| `/admin/access` | Admin(접근 권한) | admin |
|
||||
| `/admin/tickets` | Admin(운영 티켓 처리 큐: 접수, 우선순위, 상태 변경) | admin |
|
||||
| `/settings` | Settings | 3역할 공통 |
|
||||
|
||||
- `RequireAuth`가 `lib/auth`의 AuthContext로 가드하고, 권한 불일치 시 역할 홈(`roleHomePath`)으로 보낸다.
|
||||
|
|
@ -380,16 +461,27 @@ React 19 + Vite. 라우팅은 `apps/web/src/App.tsx`(react-router-dom).
|
|||
- `apiFetch`/`api`(get/post/put/del) — 표준 JSON, 에러는 `ApiError`로 정규화, 항상 `credentials:"include"`.
|
||||
- SSE: `openSessionStream(sessionId, text, handlers)` — `fetch` 스트림을 직접 라인 파싱해
|
||||
`token | done | safety | ping | error` 이벤트를 콜백으로 전달.
|
||||
- 도메인 헬퍼: `sessionApi`(list/get/start/turn/end/review/stream), `personaApi`, `personaReviewApi`,
|
||||
- 도메인 헬퍼: `sessionApi`(dashboard/list/get/start/turn/liveCoach/liveCoachHistory/end/review/stream/archive/restore), `personaApi`, `personaReviewApi`,
|
||||
`adminApi`, `adminEngineApi`, `teacherApi`, `userApi`. 주요 응답 타입은 FastAPI OpenAPI에서 생성한
|
||||
`src/lib/api.gen.ts`의 `ApiSchema<...>` alias를 사용하고, generated optional 배열은 화면 렌더링 계층에서
|
||||
빈 배열 fallback으로 흡수한다. 세션 시작/상세/턴 응답의 stage는 backend `StageLabel`
|
||||
enum(`라포|탐색|개입|정리`)에서 생성한 union을 `SessionStage`로 사용한다.
|
||||
- `sessionApi.createShare`/`revokeShare` — 학습자 리뷰 화면에서 공개 공유 URL 생성/폐기를 호출한다.
|
||||
`SessionReview`은 종료된 회기에서만 "공유 URL 복사" 버튼을 노출한다.
|
||||
- dev 환경: `vite.config`가 `/api` → `http://127.0.0.1:8000` 프록시(`/api` 프리픽스 제거).
|
||||
배포 호스트(`vignette.chanpaca.net`, `*.pages.dev`)에서는 `api-vignette.chanpaca.net`을 직접 가리킨다.
|
||||
|
||||
세션 화면 흐름(요약): 학습자 발화 입력 → `sessionApi.stream(id, text)`(SSE) 또는 `turn`(동기) →
|
||||
토큰 누적 표시 → 회기 종료(`end`) → `/review`에서 `sessionApi.review(id)`로 리뷰 표시.
|
||||
토큰 누적 표시 → 코칭 모드면 `sessionApi.liveCoach(...)` + `liveCoachHistory(...)`로 아바타 말풍선/발화별
|
||||
코칭 마커 갱신 → 회기 종료(`end`) → `/review`에서 `sessionApi.review(id)`로 리뷰 표시.
|
||||
|
||||
정적 SEO/GEO 자산:
|
||||
|
||||
- `apps/web/index.html` — 기본 description/canonical/Open Graph/Twitter Card/JSON-LD(WebApplication).
|
||||
- `apps/web/public/robots.txt` — 인증 내부 경로(`/pending`, `/learn`, `/teach`, `/admin`, `/settings`, `/dev`) 색인 차단.
|
||||
AI 검색용 `OAI-SearchBot`, `ChatGPT-User`에도 같은 경계를 명시한다.
|
||||
- `apps/web/public/sitemap.xml` — 공개 entry point만 포함한다.
|
||||
- `apps/web/public/llms.txt` — AI 검색/요약용 공개 설명, 공유 URL의 no-transcript 정책, 비공개 경로 경계를 명시한다.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -401,19 +493,37 @@ DB는 PostgreSQL 16 + pgvector(단일 SoR). 초기화 SQL은 `infra/db/init/`에
|
|||
|
||||
### 5.1 app 스키마 (`infra/db/init/02_schema.sql`)
|
||||
|
||||
- **인적 주체** `app.app_user` — RBAC 앵커(role: learner/instructor/admin, cohort, consent_at).
|
||||
- **인적 주체** `app.app_user` — RBAC 앵커(role: learner/teacher/admin, cohort, consent_at)와
|
||||
계정 승인 상태(`account_status`: pending/approved/suspended), 최초 온보딩 프로필(닉네임, 자기소개,
|
||||
아바타 URL, 이름, 소속, 학과, 학년/직위, 연락처, 주소/수령지, 약관·개인정보 동의 버전).
|
||||
- **페르소나** `app.persona_card` — 불변 버전드 카드(`UNIQUE(code, version)`, status draft→review→approved→archived,
|
||||
ccd/dsm5_dimensional/affect_baseline는 JSONB). `app.persona_voice_map`(provider-agnostic 음성),
|
||||
ccd/dsm5_dimensional/affect_baseline는 JSONB). `archived`는 카탈로그 제거용 tombstone이며 기존 세션은 `persona_id/persona_version` 핀으로 계속 해석한다.
|
||||
`app.persona_voice_map`은 같은 `persona_id/version`에 묶인 provider-agnostic 음성 설정이다. live OpenAI TTS는
|
||||
provider=`openai` row의 `voice_id`와 `base_params`를 `VoicePreset`으로 해석하고, seed materializer는 기본 OpenAI row를
|
||||
충돌 없이 생성한다. 기존 dev DB에 테이블이 없으면 `ensure_runtime_tables()`가 `persona_card` 기준 FK 구조로 보강한다.
|
||||
`app.counselor_profile`(상담사 AI).
|
||||
- **라벨 코드테이블**(taxonomy 3축) — `stage_def`, `technique_label_def`, `client_state_def`, `scale_def`.
|
||||
- **세션** `app.sessions` — case_id/persona_id/persona_version 핀, stage_path. `case_id`는
|
||||
(persona_id, learner_id) 복합 인스턴스 식별.
|
||||
- **회기 보관 상태** `app.session_archive_state` — `session_id`/`learner_id` 단위의 학습자 보기 상태.
|
||||
`archived_at`/`updated_at`만 저장하고 restore 시 row를 삭제한다. RLS는 학습자 본인의 보관/복원과
|
||||
teacher/admin 조회만 허용하며, 원 세션·발화·리뷰·공유 링크는 보존한다.
|
||||
- **발화** `app.turns` — ② EPISODIC append-only. `text`(마스킹 원문)/`text_masked`(외부전송용),
|
||||
`visible_to TEXT[]`(기본 `{client,counselor,evaluator}`), salience/is_pinned/contradicts,
|
||||
음성 paralinguistic(audio_ref/silence_ms/speech_rate/barge_in). `turn_technique`/`turn_client_state`
|
||||
다대다, `supervisor_comment`(rationale/critique + intent_deviation JSONB), `safety_events`.
|
||||
- **사례개념화 제출물** `app.case_worksheet` — 회기 리뷰의 축어록 기반 자동 초안을 학습자가 편집해 저장한 JSONB.
|
||||
`session_id` 단위 upsert이며, `GET /review`에서 자동 초안보다 우선된다.
|
||||
- **교수자 검토 상태** `app.session_review_status` — 교수자/관리자의 회기 검토 상태, 메모, 검토 시각을
|
||||
`session_id` 단위로 저장한다. teacher dashboard는 `review_status/review_note/reviewed_at`를 내려 보내며,
|
||||
검토 완료된 회기는 pending queue에서 제외한다.
|
||||
- **공개 공유 카드** `app.session_share_link` — `session_id` 단위 공개 토큰 해시와 sanitized preview payload.
|
||||
RLS는 학습자 본인 생성/폐기와 teacher/admin 열람, public route의 AI 컨텍스트 조회만 허용한다. 원문 축어록을
|
||||
저장하지 않는다.
|
||||
- **운영 콘솔** `app.admin_health_event` — 관리자 `/admin/health` 조회 시점의 서비스별 헬스 샘플을
|
||||
append-only로 남긴다. 이 값은 상시 모니터링 SLA가 아니라 운영 콘솔이 관측한 최근 샘플 이력이다.
|
||||
`app.support_ticket`은 인증 사용자가 제출한 문제·불만·장애 티켓을 저장하고, 관리자는 `/admin/tickets`에서
|
||||
우선순위와 처리 상태를 갱신한다. RLS는 티켓 제출자 본인 insert/select와 admin 전체 처리만 허용한다.
|
||||
- **메모리 4계층**:
|
||||
- ① WORKING `app.session_state` — 상태머신 수치 체크포인트(매 턴 UPSERT, openness/ideation CHECK 제약).
|
||||
- ② EPISODIC 임베딩 `app.turn_embedding` — BGE-M3 dense `vector(1024)` + sparse, HNSW 인덱스.
|
||||
|
|
@ -466,10 +576,35 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
|
|||
- OAuth callback 실패는 authorization code, token, raw email을 남기지 않고 reason/status/도메인 수준 정보만
|
||||
서버 로그에 남긴다. 프론트는 `token_exchange_failed`, `invalid_state`, provider error(`access_denied`/`provider_error`),
|
||||
identity claim 실패를 구분하고 실패 reason code를 화면에 함께 표시한다.
|
||||
- 역할은 `AUTH_TEACHER_EMAILS`/`AUTH_ADMIN_EMAILS` email allowlist로 1차 판정한다. 코호트는
|
||||
- 역할은 `AUTH_TEACHER_EMAILS`/`AUTH_ADMIN_EMAILS` email allowlist로 1차 판정한다.
|
||||
`admin_access`는 기본 역할과 별도인 관리자 콘솔 진입 권한이며, 슈퍼 관리자만 `/admin/users`에서
|
||||
부여·회수할 수 있다. `AUTH_SUPER_ADMIN_EMAILS`는 항상 관리자 콘솔 접근, 학습자·교수자 공간 접근,
|
||||
approved 상태를 부여하는 신뢰 루트다(기본 `yunchan@twentyoz.kr`, `hoonjungkoo@hs.ac.kr`). 코호트는
|
||||
`AUTH_EMAIL_COHORT_MAP`과 `AUTH_DOMAIN_COHORT_MAP` 설정, SAML fixture의 `cohort` claim을 합쳐
|
||||
`cohort_ids`로 세션에 저장한다. 관리 사용자 `app_user.external_id`는 provider subject 기반
|
||||
(`google:`/`saml:`/`dev:`)으로 저장해 email 변경 리스크를 줄인다.
|
||||
- `AUTH_ALLOWED_EMAIL_DOMAINS`는 기본 도메인 게이트다. 단, 슈퍼 관리자/관리자가 `/admin/users`에
|
||||
미리 만든 정확한 이메일은 도메인 밖이어도 Google/SAML/dev-login의 이메일 검증을 통과한다.
|
||||
이 예외는 도메인 전체를 열지 않고, provider 로그인 시 기존 `email:<주소>` 관리 row를
|
||||
`google:`/`saml:`/`dev:` external_id로 이어받아 역할·코호트·승인 상태를 보존한다.
|
||||
- 신규 Google/SAML 사용자는 기본적으로 `account_status=pending`으로 생성된다
|
||||
(`AUTH_NEW_USER_DEFAULT_STATUS`). `/auth/me`만 pending 상태 확인용으로 열어두고, 그 외 REST/WS 기능
|
||||
경로는 `account_pending` 또는 인증 실패로 막는다. `AUTH_APPROVED_EMAILS`, 관리자 생성 사용자,
|
||||
`AUTH_SUPER_ADMIN_EMAILS`, 로컬 dev-login(`external_id=dev:*`)은 자동 approved다.
|
||||
- 프론트 전역 `PendingApprovalGate`는 pending/suspended 사용자를 `/pending`으로 돌리고, 관리자는
|
||||
`/admin/users`의 가입 승인 탭에서 pending 계정을 approved 또는 suspended로 변경한다.
|
||||
- Google/SAML/dev-login 성공 직후 `onboarding_completed_at`이 없거나 닉네임/자기소개가 비어 있으면
|
||||
프론트 전역 `OnboardingGate`가 `/onboarding` 외 모든 앱 URL(`/learn`, `/settings`, `/admin`,
|
||||
`/dev/avatar-preview`, `/login` 포함)을 `/onboarding`으로 돌린다. 온보딩 중에는 공용 셸 메뉴를
|
||||
렌더링하지 않고, 빈 상태 카드 대신 가입 직후 사용자 정보를 입력하는 단순 폼만 보여준다.
|
||||
이 화면은 이메일을 다시 받지 않고 닉네임, 자기소개,
|
||||
선택 아바타 이미지, 이름, 소속, 학과, 학년/직위, 연락처, 주소/수령지와 서비스 이용약관·개인정보
|
||||
처리방침 초안 동의를 저장한다. 아바타 파일은 `POST /users/me/avatar`가 MIME/시그니처/3MB 제한을
|
||||
확인한 뒤 `USER_UPLOAD_DIR/profile-avatars`에 저장하고 URL만 `app_user.avatar_url`에 보관한다.
|
||||
학습자 `POST /sessions`와 dev 음성 persona 시작은 온보딩 완료 후에만 허용하며, 온보딩 저장 시
|
||||
learner `consent_at`도 함께 세팅한다.
|
||||
- 관리자/교수자 권한은 온보딩 화면에서 신청받지 않는다. 서버는 `AUTH_ADMIN_EMAILS` /
|
||||
`AUTH_TEACHER_EMAILS` allowlist와 관리자 사용자 관리 경로로만 역할을 부여한다.
|
||||
- 로컬/Tailnet dev는 dev-login을 사용한다. 공개 `OAUTH_REDIRECT_URI`가 로컬/Tailnet 세션이 아니라 public API
|
||||
세션으로 돌아가는 혼선을 막기 위해 dev-origin의 Google 직접 시작은 `local_oauth_unavailable`로 차단한다.
|
||||
|
||||
|
|
@ -540,10 +675,15 @@ npm run build # vite
|
|||
npm run e2e # Playwright (web+api+DB 스택 필요)
|
||||
```
|
||||
|
||||
### 7.5 Docker compose (DB + API)
|
||||
### 7.5 Docker compose (DB + API + Web + Proxy)
|
||||
|
||||
`infra/docker-compose.yml`(db: pgvector pg16 + api). Docker Desktop 필요. DB 컨테이너 기동 시
|
||||
`infra/db/init/*.sql`이 순서대로 적용된다.
|
||||
`infra/docker-compose.yml`(db: pgvector pg16 + api + web + proxy). Docker Desktop 필요. DB 컨테이너 기동 시
|
||||
`infra/db/init/*.sql`이 순서대로 적용된다. API 이미지는 저장소 루트 컨텍스트에서 빌드해
|
||||
`data/personas`를 포함하고, 사용자 업로드는 `apiuploads` 볼륨(`/app/uploads`)에 보관한다.
|
||||
RAG 임베딩/리랭커 의존성은 기본 슬림 이미지에 넣지 않고 `INSTALL_RAG=true` build arg로만 설치한다.
|
||||
라이브 코칭 source pack(`data/kb/live_coaching_*`)도 같은 이미지 입력이므로 clean clone/배포지에서 누락되면
|
||||
`scripts/check-deploy-preflight.py`가 실패해야 정상이다. DB schema drift는 API startup에서 owner 권한을 키워
|
||||
수습하지 않고, owner-run init/migration 후 app-role DSN으로 preflight를 통과시킨 뒤 API를 띄운다.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Vignette(AI 심리상담 시뮬레이션 훈련 플랫폼)를 로컬에서 띄
|
|||
- `apps/api` — FastAPI 백엔드 (Python)
|
||||
- `apps/api/engine_gateway` — AI 턴 생성용 엔진 게이트웨이(별도 프로세스, 포트 9099)
|
||||
- `apps/web` — React 19 + Vite 프런트엔드
|
||||
- `infra` — Docker Compose 스택(pgvector pg16 + api + web + rag + proxy)
|
||||
- `infra` — Docker Compose 스택(pgvector pg16 + api + web + proxy)
|
||||
- `scripts` — 운영/점검 스크립트
|
||||
|
||||
핵심 구성요소: 학습자(상담수련생)가 AI 내담자 페르소나와 회기를 진행하고, 종료 후 회기 리뷰 피드백을 받는다.
|
||||
|
|
@ -87,7 +87,7 @@ 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`를 포함한다.
|
||||
`pydantic-settings`, `python-multipart`, `httpx`, `sse-starlette`를 exact version으로 고정한다.
|
||||
엔진 게이트웨이도 `fastapi`+`uvicorn`만 쓰므로 위 설치로 함께 충족된다.
|
||||
(Presidio PII 마스킹은 선택 의존성이라 기본 제외 — 없으면 정규식 폴백으로 자동 degrade.)
|
||||
|
||||
|
|
@ -117,7 +117,10 @@ npm install
|
|||
| `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 포함 검증) |
|
||||
| `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` 필요, 프로덕션 금지 |
|
||||
|
|
@ -136,7 +139,7 @@ python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
|
|||
|
||||
### 2.3 seed 페르소나로 띄우기 (선택)
|
||||
|
||||
기본값은 seed 미적재(`AUTO_SEED_PERSONAS=false`)다. 내장 페르소나(P1~P3)를 메모리/DB에 올려서
|
||||
기본값은 seed 미적재(`AUTO_SEED_PERSONAS=false`)다. 시스템 페르소나(P1~P3)와 저장소 페르소나(P4~P7)를 DB 카탈로그에 올려서
|
||||
바로 회기를 만들고 싶으면 실행 전에 두 플래그를 켠다.
|
||||
|
||||
```powershell
|
||||
|
|
@ -147,6 +150,7 @@ 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 기동 (정상 동작)
|
||||
|
|
@ -188,10 +192,19 @@ curl.exe http://127.0.0.1:8000/health
|
|||
- 본문: `{ "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`. 다른 도메인이면
|
||||
> `403 email domain is not allowed`.
|
||||
> 주의(이메일 도메인): 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 + 세션 쿠키
|
||||
|
||||
|
|
@ -233,6 +246,33 @@ curl.exe http://127.0.0.1:8000/auth/me -b cookies.txt
|
|||
> 로컬/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가 없으면 실패하게 둔다.
|
||||
|
||||
```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이 있다는 뜻이다.
|
||||
임베딩 모델이 없으면 `degraded=true`로 BM25-only 색인이 되지만, 라이브 코칭 기본 로컬 근거는 계속 동작한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 웹(프런트엔드) 실행
|
||||
|
|
@ -311,7 +351,7 @@ python scripts\probe-engine-gateway.py --json # 기계 판독용
|
|||
|
||||
## 6. (선택) Docker Compose 전체 스택
|
||||
|
||||
DB까지 포함한 통합 실행은 `infra/docker-compose.yml`(db pgvector pg16 + api + web + rag + proxy)을 쓴다.
|
||||
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 키 등을 채워야 한다.
|
||||
|
||||
|
|
@ -321,8 +361,18 @@ 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`를 설정한다.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -331,8 +381,8 @@ docker compose up -d # 전체
|
|||
```powershell
|
||||
# 백엔드 (apps/api)
|
||||
cd apps\api
|
||||
python -m pytest app\ -q # 현재 113 pass
|
||||
python -m pytest engine_gateway\ -q # 7 pass
|
||||
python -m pytest app/ -q # 현재 145 pass
|
||||
python -m pytest engine_gateway\ -q # 현재 9 pass
|
||||
|
||||
# 웹 (apps/web)
|
||||
cd apps\web
|
||||
|
|
@ -363,7 +413,8 @@ npm run e2e # Playwright — web+api+DB 스택 필요
|
|||
요청이 로컬 Origin/Host인지 확인. `apps/api`에서 uvicorn을 실행해 `.env`가 로드됐는지도 확인.
|
||||
|
||||
- **dev-login이 403 (`email domain is not allowed`)**: 이메일 도메인이
|
||||
`AUTH_ALLOWED_EMAIL_DOMAINS`에 없음. `learner@hs.ac.kr` 같은 허용 도메인을 쓴다.
|
||||
`AUTH_ALLOWED_EMAIL_DOMAINS`에 없고, `/admin/users`에 정확히 등록된 관리 사용자도 아니다.
|
||||
`learner@hs.ac.kr` 같은 허용 도메인을 쓰거나 관리자가 해당 이메일을 먼저 등록한다.
|
||||
|
||||
- **`/auth/me`가 401**: 쿠키가 전달되지 않음. curl은 `-c`/`-b`로 쿠키를 저장·재사용하고,
|
||||
PowerShell은 `-SessionVariable`/`-WebSession`을 쓴다. 브라우저는 프록시(5173) 경유로 호출해야
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue