전 저장소 리팩터링과 SSOT 정비
This commit is contained in:
parent
14ecbd4e7d
commit
3dfddcac6f
173 changed files with 19679 additions and 6952 deletions
|
|
@ -25,6 +25,8 @@ Vignette(AI 심리상담 시뮬레이션 훈련 플랫폼)의 시스템 아키
|
|||
자살 수단/방법 정보는 출력 가드레일이 차단한다.
|
||||
- **주입형(hook) 경계**: 오케스트레이터는 평가/로깅 함수를 *주입*받는다. `services/orchestrator.py`는
|
||||
`services/evaluator.py`를 import 하지 않는다(소유권 분리).
|
||||
- **LLM 감사 경계**: evaluator/orchestrator의 생성 시간·토큰·비용 기록은 `services/llm_audit.py`의
|
||||
`generate_with_audit()`가 소유한다. 생성 성공을 감사 저장 성공으로 오인하지 않고 호출자가 degraded 의미를 결정한다.
|
||||
- **degraded 폴백**: DB(단일 SoR)가 없어도 `app/store.py` in-memory 미러로 1턴이 돈다.
|
||||
|
||||
### 1.1 모노레포 레이아웃
|
||||
|
|
@ -254,8 +256,17 @@ RBAC×AIView로 차단된다. 이 모듈은 평가 신호만 산출한다.
|
|||
형태가 아니라 `quota`와 `credit_events`를 포함한 짧은 카드 계약이다. UI는 코칭 아바타 말풍선,
|
||||
근거 모달, 발화별 코칭 이력 오버레이로 표시한다.
|
||||
- 회기별 코칭 기회는 기본/최대 3개다. `POST /sessions/{id}/live-coach` 사용 시 `use/-1` 이벤트를 남기고,
|
||||
턴 평가가 `appropriateness=pos`, `rapport_signal>=0.35`, 그리고 단계 전환 또는 `effective_openness`
|
||||
상승을 동시에 만족하면 `recharge/+1` 이벤트로 1개를 재충전한다(최대 3개). 사용권 없음은 409로 막는다.
|
||||
충전은 2026-07-14 재설계된 게이트를 쓴다(`turn_runtime.should_recharge_live_coach_credit`):
|
||||
① 성과 충전 — `appropriateness=pos` + `rapport_signal>=0.15` + (단계 전환 또는 개방도 +0.01),
|
||||
또는 `neutral` + `rapport_signal>=0.35` + 개방도 상승 ② 페이싱 충전 — 평가 신호와 무관하게
|
||||
6턴마다 1개(평가 실패여도 충전, 순감 구조 방지). 사용권 없음은 409로 막는다.
|
||||
- 프론트 트리거: 코칭 화면(`feedbackMode=coached`)이 아닐 때 완료된 턴은 기회를 소모하지 않고
|
||||
`pendingCoachTurn`으로 대기하며, 컨트롤바 위 컨텍스추얼 넛지 칩("방금 발화에 코치 제안이 있어요")으로
|
||||
안내한다. 코칭 화면을 열면 대기 턴에 대한 코칭을 즉시 요청한다.
|
||||
- 코칭 프롬프트에는 이번 회기 목표(`goal_stages`)와 직전 코칭 2건(title/focus)을 주입해
|
||||
목표 정렬·조언 반복 방지를 유도하고, 규칙 폴백도 단계별 기본 다음 발화를 쓴다.
|
||||
- `record_llm_call_audit`는 dev degraded(무DB) 기동을 "기록 실패"로 보지 않는다
|
||||
(`runtime_fallback_allowed()`면 True). durable 환경의 실제 기록 실패만 코칭을 degraded로 강등한다.
|
||||
- 전달된 코칭과 충전/사용 기록은 `app.live_coach_events`에 저장된다. 원문 축어록을 중복 저장하지 않고
|
||||
PII 마스킹된 짧은 learner/client excerpt, 코칭 payload, `event_type/credit_delta/credit_balance/reason`만
|
||||
저장한다. DB 미가용 dev에서는 런타임 캐시로 폴백한다.
|
||||
|
|
@ -339,6 +350,14 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
|
|||
기본값을 쓰되 학습자가 `humanistic`/`cbt`/`integrative` 중 하나를 명시 선택해 기존
|
||||
`theory_mode` 계약으로 보낸다. DB가 없으면
|
||||
`runtime_fallback_allowed()` 확인 후 `store.create(...)`로 in-proc 생성.
|
||||
2026-07-13 한신대 회의 P1 반영: 요청에 이번 회기 목표 단계 `goal_stages`(StageLabel 1~4개 —
|
||||
회의 권장은 2개 수준, 소유자 지시(2026-07-15)로 4개까지 허용. 중복 제거·최대 4 검증,
|
||||
`app.sessions.session_goals` JSONB 저장)를 받고, 응답에
|
||||
`started_at`/`goal_stages`/`duration_limit_seconds`/`warning_before_end_seconds`를 돌려준다.
|
||||
회기 종료는 단계 완수가 아니라 **시간 기반**이다 — 프론트가 60분 타이머·10분 전 알람·만료 시
|
||||
정리 유도를 주도하고, 서버는 제한+유예(`SESSION_DURATION_MINUTES`+`SESSION_OVERTIME_GRACE_MINUTES`)
|
||||
초과 시 신규 턴을 `409 session_time_over`로 거부한다(`_ensure_turn_time_allowed`, 음성 WS 동일).
|
||||
목표를 달성해도 시간 내에는 계속 진행한다(상태머신은 채점·표시용으로만 유지).
|
||||
- `POST /sessions/{id}/turn` — 동기 경로. `prepare_turn` → `run_turn_generate` →
|
||||
학습자 발화 turn(speaker=`counselor`, 원문 `text` + `text_masked`) + 내담자 응답 turn(speaker=`client`)
|
||||
순차 append → 상태 갱신. `EngineError`는 503으로 변환.
|
||||
|
|
@ -355,6 +374,14 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
|
|||
임베딩 모델이 없으면 BM25-only degraded 색인으로 진행한다.
|
||||
- `POST /sessions/{id}/end` — `memory.make_carry_over(...)` → 세션 종료 + carry 준비 →
|
||||
`_schedule_session_evaluation(sess)`로 deep-loop 평가를 비동기 태스크로 발사.
|
||||
- 진행도 파생(P2, 2026-07-14): `session_read_model.build_session_progress(state, prev_rapport_credit,
|
||||
goal_stages)`가 상태머신 수치를 학습자-안전 %로 파생한다 — 단계별 누적 게이지(현재 단계는
|
||||
`rapport_credit / STAGE_ADVANCE_RAPPORT` 기준, 지나온 단계 100, 전이 대기 99 캡), 라포 누적
|
||||
(`/0.55` 전 주기 기준)과 이번 회기 증가분(`prev_rapport_credit` 대비), 방어(저항)·유효 개방도 %.
|
||||
노출 지점: `SessionDetailResponse.progress`, `TurnResponse.progress`, 스트림 `done` payload,
|
||||
음성 WS `reply`, 대시보드 `persona_progress[].rapport_percent`. rapport_credit이 회기 간 ×0.7
|
||||
이월되므로 게이지가 회기를 건너 누적된다(초심 상담자 학습 신호 — 회의 P2 의도). ideation·CCD는
|
||||
계속 비노출.
|
||||
- `GET /sessions/dashboard` — 학습자 본인 세션만 `include_turn_evaluation=true`로 집계해
|
||||
`overview/growth/persona_progress/achievements/recent_feedback`를 반환한다. `overview.archived_sessions`는
|
||||
학습자별 보관 상태(`app.session_archive_state`) 기준 카운트이며, 보관된 종료 회기는 리뷰 대기 행동 큐에서
|
||||
|
|
@ -494,8 +521,16 @@ HTTP error mapping을 유지한다.
|
|||
- `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만 맡고,
|
||||
실제 저작은 개요·임상·저항·회기·말투·안전·프롬프트 탭으로 분리한다.
|
||||
- 페르소나 워크스페이스: `/teach/personas`는 teacher/admin 전용 운영·저작 작업면이다. 상단 horizontal tab이
|
||||
`대시보드`(공개/활성 페르소나·학습 인원·세션·평가/라포·검수 요약), `카탈로그`(공개 규칙·구성·버전),
|
||||
`페르소나`(검색/필터 가능한 전체 table + 내부 `검수 현황` 탭)를 나눈다. 행을 누르면 소개·학습 현황·설정
|
||||
drilldown으로 들어가며, 수정/신규 작성은 URL query 상태의 독립 작업면에서 자료→초안→설정→검토 4단계를
|
||||
사용한다. 작성 중간 저장은 브라우저 임시 저장을 항상 남기고, 기본 식별자가 채워지면 기존 draft API에도 저장한다.
|
||||
구조화 편집 자체는 개요·임상·저항·회기·말투·안전·프롬프트 탭 계약을 유지한다.
|
||||
- 자유 양식 표 업로드(2026-07-13 회의 P4): `POST /personas/sources/upload`는 xlsx/xlsm/csv 파일을
|
||||
받아 `services/tabular_ingest.extract_tabular_text()`로 결정론 평탄화(라벨: 값 쌍, 시트 구분,
|
||||
cp949 폴백, .xls 거부)한 뒤 아래 `/personas/sources` 등록 경로를 그대로 재사용한다.
|
||||
업로드 원본 바이트는 핸들러 메모리에서만 파싱하고 저장하지 않는다(원본 파기 — hash-only 증거만 남음).
|
||||
- RAG 첨부 SSOT: `POST /personas/sources`는 첨부/붙여넣기 자료를 `mask_pii()` 후
|
||||
raw 원문 hash-only 증거는 `kb.raw_source_artifact`에 따로 기록하고, sanitized 파생본만
|
||||
`kb.source/document/chunk`에 evaluator 전용 근거(`visible_to=['evaluator']`, `sensitivity=2`)로 등록한다.
|
||||
|
|
@ -567,12 +602,16 @@ React 19 + Vite. 라우팅은 `apps/web/src/App.tsx`(react-router-dom).
|
|||
| `/teach/personas` | PersonaStudio(페르소나 저작·검수) | teacher/admin |
|
||||
| `/teach/session/:sessionId/review` | SessionReview(교수자 읽기 전용 회기 검토) | teacher/admin |
|
||||
| `/admin` | Admin(운영 홈) | admin |
|
||||
| `/admin/ai` | AdminAi(AI 엔진 설정, 기간별 DB 비용·토큰·계량 커버리지, provider/model 원장, 평가 캐시 효율) | admin |
|
||||
| `/admin/users` | Admin(사용자 관리) | admin |
|
||||
| `/admin/access` | Admin(접근 권한) | admin |
|
||||
| `/admin/tickets` | Admin(운영 티켓 처리 큐: 접수, 필터, 카테고리 큐, 우선순위, 수동 상태 변경 감사) | admin |
|
||||
| `/settings` | Settings | 3역할 공통 |
|
||||
|
||||
- `RequireAuth`가 `lib/auth`의 AuthContext로 가드하고, 권한 불일치 시 역할 홈(`roleHomePath`)으로 보낸다.
|
||||
- `App.tsx`는 모든 역할 페이지를 `lazy()`로 로드하고 공통 `Suspense` 부트 경계를 사용한다. 페이지별 순수 표시 계산은
|
||||
`pages/*/model.ts`, 브라우저 음성 캡처는 `pages/session/voiceCapture.ts`, 페르소나 이름·난도·아바타 팔레트는
|
||||
`lib/personaViewModel.ts`가 소유해 라우트 컴포넌트의 API/상태/렌더 책임과 분리한다.
|
||||
- API 클라이언트 `src/lib/api.ts`:
|
||||
- `apiFetch`/`api`(get/post/put/del) — 표준 JSON, 에러는 `ApiError`로 정규화, 항상 `credentials:"include"`.
|
||||
- SSE: `openSessionStream(sessionId, text, handlers)` — `fetch` 스트림을 직접 라인 파싱해
|
||||
|
|
@ -593,6 +632,8 @@ React 19 + Vite. 라우팅은 `apps/web/src/App.tsx`(react-router-dom).
|
|||
(예: `pages/login/login.css`, `pages/learner-home.css`, `pages/session/session.css`).
|
||||
TSX 안에 `<style>{..._CSS}</style>` template literal을 두지 않는다. 새 화면은 스타일을
|
||||
페이지/기능 CSS로 분리하고, 반복 JSX는 작은 presentational component로 빼서 인증·데이터 상태 로직과 섞지 않는다.
|
||||
- 공통 UI/셸 CSS의 raw color는 0개를 강제한다. 몰입형 세션·인증·아바타 아트처럼 전역 의미 토큰이 아닌 국소 색은
|
||||
`check:design-ssot`의 파일별 고정 예산을 넘길 수 없어 예외가 다른 화면으로 확산되지 않는다.
|
||||
- 로그인 진입 화면은 `pages/login/LoginBrand.tsx`, `LoginPanel.tsx`, `login.css`로 분리되어
|
||||
`Login.tsx`는 OAuth/dev-login 상태와 라우팅만 소유한다.
|
||||
|
||||
|
|
@ -728,8 +769,9 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
|
|||
- 프론트의 최초 진입 경로(`initialPathForUser`)는 pending이면 `/pending`, 온보딩 미완료 일반 사용자는
|
||||
`/onboarding`, 관리자 콘솔 접근권이 있는 사용자는 기본 역할이 learner/teacher여도 `/admin`을 우선한다.
|
||||
역할 전환용 `roleHomePath`는 그대로 역할별 홈(`/learn`, `/teach`, `/admin`)만 소유한다.
|
||||
- SPA 라우트 전환은 `App.tsx`가 `pathname` 변경마다 문서 스크롤을 맨 위로 복원한다. 긴 관리자 목록에서
|
||||
짧은 `/admin/access` 정책 화면으로 이동할 때 이전 `scrollY`가 유지되어 sticky 셸만 보이는 빈 화면을 막기 위한 계약이다.
|
||||
- SPA 라우트 전환은 `App.tsx`가 `pathname` 변경마다 문서 스크롤을 맨 위로 복원한다. 또한 브라우저
|
||||
`history.scrollRestoration`을 `manual`로 두고 `pageshow`에서도 다시 복원해, 재부팅 뒤 기존 관리자 탭을
|
||||
되살릴 때 이전 `scrollY` 때문에 sticky 셸만 보이고 본문이 화면 위로 밀리는 빈 화면을 막는다.
|
||||
- 관리자 콘솔은 shell만 남고 본문이 비는 silent failure를 허용하지 않는다. `/admin/users` 응답의
|
||||
`cohort_ids`, `active_sessions`, `created_at`와 `/admin/tickets` 응답의 `summary`, `by_status`,
|
||||
`by_priority` 같은 필드가 런타임 계약과 다르면 프론트가 안전한 기본값으로 정규화하고 `관리자 데이터 진단`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue