전 저장소 리팩터링과 SSOT 정비

This commit is contained in:
Yun Chan 2026-07-15 21:31:30 +09:00
parent 14ecbd4e7d
commit 3dfddcac6f
173 changed files with 19679 additions and 6952 deletions

View file

@ -20,7 +20,7 @@
스레드 생성, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 담당 그룹 자동 배정·우선순위 escalation·raw/rollup
보존기간 같은 운영 정책은 B3에서 이미 결정됨(수동 승인·미도입 고정). 자동 수정은 운영자 승인 전까지 실행하지 않는다.
> B1 완료 항목(셸 구분선, 세션 종료 UX/다크테마, 아바타 PSD v2, SEO/공유 카드, 레이아웃 정렬, 권한 위임,
> B1 완료 항목(셸 구분선, 세션 종료 UX/다크테마, 아바타 SVG 리그 복귀와 래스터 비활성화, SEO/공유 카드, 레이아웃 정렬, 권한 위임,
> 메일링, 아카이브 API, TTS voice map, 빈상태 레이아웃, 평가 실패 복구 UX, SSE 저장, live-coach 표면화,
> 음성 메타, 위기 게이트 UI, 운영 티켓/헬스 샘플러 등)의 상세·검증 로그는 아카이브 full-history 참조.

View file

@ -3,20 +3,17 @@
> 다른 세션에서 이어서 작업하기 위한 인계. imagegen(gpt-image-2)+BiRefNet+파츠 분리 리깅 파이프라인 전체를 여기서 참조.
> 상세 원칙은 `AGENTS.md` §4.
> 2026-06-27 19:45 추가: 사용자 제공 PSD `라투디 여캐_ver2.psd`를 기준으로 새 파츠 세트 `seoyeon-live2d-psd-v2`를 만들고 P1 실제 세션 기본값으로 연결했다. 이 세트는 컨셉 보드 크롭이 아니라 PSD 레이어 기반이며, `sad` 표정에서 울상 눈썹·우는 입·눈물 파츠를 별도로 합성한다. `p1-concept`는 기본값으로 쓰지 않는다.
> **2026-07-13 상태 변경 — PAUSED:** 사용자 피드백에 따라 서연을 포함한 생성 이미지/PSD 래스터 파츠 리그는 실제 화면에서 전부 비활성화했다. 현재 `ClientAvatar`는 모든 페르소나에서 기존 SVG 도형 기반 파라미터 리그만 렌더링한다. 래스터 렌더러·원본·파츠 자산은 재검토 가능하도록 보존하지만 앱 배선에는 연결하지 않는다.
---
## 0. 한 줄 요약
파이프라인·렌더러·dev 미리보기까지 **완성**됐으나, 서연 아트가 **잘못된 참조(Image #1=긴 머리)**로 만들어져, **진짜 목표 Image #2(짧은 갈색 보브, 정면)**로 **재생성**이 남았다.
래스터 파츠 파이프라인은 보존 상태로 중단했고, 현재 제품과 dev 미리보기는 기존 SVG 도형 기반 파라미터 리그만 사용한다.
---
## 1. ⚠️ 다음 세션이 가장 먼저 할 일 (핵심)
1. **목표 재확인**: 진짜 타겟은 **Image #2** = `D:\workspace\GLM\.claude-glm\image-cache\73cbbea0-5c27-4ced-91db-16d7a8df602f\2.png`
(단일 캐릭터, **짧은 갈색 보브**, 정면 — 현재 서연의 긴 머리와 다름. 사용자가 "잉??" 한 원인).
- 비전(analyze_image) 도구가 다중 패널/캐릭터를 자주 혼동했으므로, **재생성 전 사용자에게 Image #2 해석(짧은 보브, 의상 색 등)을 한 번 확인**할 것.
2. **Image #2를 `-i` 참조로 재생성**하면 gpt-image-2가 캐릭터를 그대로 따라함(텍스트 묘사 의존 X). 아트 디렉션은 2.png에 맡긴다.
## 1. 재개 조건
소유자가 래스터 아바타 재도입을 명시적으로 결정하기 전에는 Image #2 재생성, 파츠 보정, 앱 재연결을 진행하지 않는다. 재개 시에도 먼저 별도 미리보기에서 시각 수용 확인을 받아야 한다.
## 2. 완료된 것 (재사용 가능)
- **이미지 생성**: `~/.codex/imagegen-headless/codex_imagegen.sh`(gpt-image-2, ChatGPT 구독). 검증 완료.
@ -30,17 +27,16 @@
- **렌더러**: `apps/web/src/components/avatar/RasterBust.tsx`
- 파츠 분리: `base` + `upperface-<표정>`(눈썹+눈) + `eyelid-closed`(깜빡임, 1-blink) + `mouth-<표정>` + `mouth-open`(립싱크, RMS).
- 28표정→클러스터 매핑(`VARIANT_FOR_EXPRESSION`) 내장.
- **연결 배선**:
- `persona.ts`: `AvatarPersona.rasterArtSet?: string` 추가됨.
- `Session.tsx`: 현재 실제 세션 기본값은 `PERSONA_AVATAR_LOOKS.P1 = { rasterArtSet: "seoyeon-live2d-psd-v2", expressionBias: "sad", ... }` + avatar 객체에 `rasterArtSet` 전달.
- `ClientAvatar.tsx`: `useRaster` 분기(rasterArtSet 있으면 `RasterBust`, 없으면 기존 SVG). `data-render-mode` 속성 추가.
- `App.tsx`: `/dev/avatar-preview` 라우트(인증 없음).
- **dev 미리보기**: `apps/web/src/pages/AvatarPreview.tsx` 기본값은 `seoyeon-live2d-psd-v2`. `?rig=psb`, `?rig=v3`, `?rig=safe`로 과거 세트 비교 가능.
- **현재 연결 상태**:
- `persona.ts`: `AvatarPersona.rasterArtSet` 계약 제거.
- `Session.tsx`·`LearnerHome.tsx`: P1/P4~P7 래스터 아트셋 배선 제거.
- `ClientAvatar.tsx`: 항상 SVG 파라미터 리그 렌더, `data-render-mode="svg"` 고정.
- `App.tsx`: `/dev/avatar-preview`는 SVG 파라미터 리그 점검 페이지로 유지.
- **dev 미리보기**: `apps/web/src/pages/AvatarPreview.tsx`는 서연 SVG 도형 리그의 대표 표정과 모션만 표시한다.
- **스크린샷 도구**: `apps/web/scripts/avatar-shot.mjs` (`BASE_URL=http://localhost:<port> node ...`).
- **현재 실제 세션 에셋**: `apps/web/public/avatar/seoyeon-live2d-psd-v2/parts/*.png`. 소스/재현 스크립트는 `docs/avatar-art/seoyeon/live2d-psd-v2/`.
- **보존 에셋(Image #1=긴 머리 버전)**: `apps/web/public/avatar/seoyeon/{neutral,sad,tired,anxious,warm,startled,eyes-closed,speaking}.png` + `parts/`, `seoyeon-live2d-psb`. 비교용으로 남기되 P1 기본값은 새 PSD v2다.
- **보존 에셋**: `apps/web/public/avatar/seoyeon-live2d-psd-v2/parts/*.png`, `apps/web/public/avatar/seoyeon/`, `seoyeon-live2d-psb`. 소스/재현 스크립트는 `docs/avatar-art/seoyeon/live2d-psd-v2/`. 현재 제품에서는 로드하지 않는다.
## 3. 남은 작업 (순서대로)
## 3. 보류된 작업 (래스터 재도입 결정 전 실행 금지)
1. Image #2(2.png) 해석 사용자 확인 → ASCII 이름으로 복사: `cp "<2.png 경로>" docs/avatar-art/seoyeon/ref-image2.png`.
2. **base 재생성**(검증용 1장):
`codex_imagegen.sh --out base-v2.png --size 1024x1280 --quality high -i ref-image2.png "Use the supplied reference as the exact character. Same character, head-and-shoulders bust, perfectly front-facing, symmetrical, neutral calm expression, flat solid light gray #C9CDD2 background, no gradient/shadow/scenery, semi-realistic soft anime. No text/watermark/hands. Bangs above eyes."`
@ -52,9 +48,9 @@
## 4. 환경 메모
- **dev 서버**: 5173 단일 인스턴스로 띄움(백그라운드). 끊기면 `cd apps/web && npm run dev -- --port 5173 --strictPort`. (5173/5174 중복 인스턴스가 "깨진 화면" 원인이었음 — 항상 5173 단일로.)
- codex CLI 0.142.2, auth_mode=chatgpt(구독). object-separation venv: `~/.venvs/object-separation` (CPU-only BiRefNet).
- 서연=P1은 API 시드(`apps/api/app/services/persona.py`). 웹엔 P1이 `PERSONA_AVATAR_LOOKS`에 있고 현재 실제 세션 기본값은 rasterArtSet="seoyeon-live2d-psd-v2"다.
- 서연=P1은 API 시드(`apps/api/app/services/persona.py`). 웹의 P1 외형/기본 정서값은 유지하지만 래스터 아트셋은 전달하지 않는다.
## 5. 미해결/결정 필요
- Image #2 캐릭터의 **의상 색/추가 디테일** (비전 도구 불안정 → 사용자 확인 필요).
- 파츠 분리 리깅 vs 통째 변주 크로스페이드 — 사용자가 "파츠 분리"를 원했으므로 현행(파츠 분리) 유지하되, 결과가 어색하면 통째 변주 방식(`RasterBust` 이전 버전, git 히스토리 참조)으로 롤백 검토.
- 래스터 아바타 재도입 여부와 방식은 현재 보류다. 현행 제품 기준은 SVG 도형 기반 파라미터 리그다.
- 실제 Live2D Cubism(.moc3) 도입 여부는 소유자 결정(편집기 저작 필요).

View file

@ -0,0 +1,70 @@
# 2026-07-13 한신대 회의 → 구현 액션 스펙
> 출처: 2026-07-13(월) 15:30 한신대 회사방문 회의 (경과보고·시연·파일럿 협의).
> 회의록 원본·전사록: vault `Projects/수업/2026/한신대 SW중심대학 산학협력 2026/회의록/2026-07-13 AI상담실습플랫폼 회의록 (경과보고·파일럿 협의).md`
> 이 문서는 그 회의의 **트웬티온스(개발) 몫 결정사항을 에이전트가 착수 가능한 스펙으로 변환**한 것.
> 진행 상태 체크는 `docs/TODO.md` §H가 소유한다. 여기는 요구사항 상세(불변 스펙)만 둔다.
## 데드라인 컨텍스트
- **7월 내**: 연구팀 페르소나·프로토콜 입력 + 테스트 완료 → 그 전에 아래 P1이 배포돼 있어야 함.
- **7월 말~8월 초**: 효과성 검증 파일럿 (대학원생 약 20명, 집중 사용).
- **9~10월**: 개발사→연구팀 점진 이관 (저장소 공유, 추가 개발 인력 합류).
---
## P1. 회기 종료 — 시간 기반 전환 (최우선, 파일럿 전 필수)
**임상 근거(회의 합의)**: 실제 상담은 한 회기에 라포·탐색·개입·정리 4단계가 모두 이뤄지지 않는다. 라포만 여러 회기도 정상. 현행 "4단계 완수 시 종료"(게임 퀘스트식)는 비현실적.
**요구사항**:
1. 세션 종료 조건을 4단계 완수 → **시간 기반(1시간)**으로 전환. 목표 미달이어도 시간 종료 시 종료. (기본 30분이던 것을 1시간으로 — LLM 응답 지연 감안 실대화 40~50분)
2. **종료 10분 전 알람** (학습자 화면).
3. **회기 시작 전 준비 페이지**: 이번 회기 목표 체크 — 4개 전부가 아니라 **2개 수준** 선택 후 시작.
4. 상태머신(라포→탐색→개입→정리)은 **채점·진행 표시용으로 유지** — 종료 조건에서만 분리. 정리 발화 시 자동 종료(기존 동작)는 시간 내 조기 종료 경로로 유지.
**완료 기준**: 1시간 경과 시 세션 종료 처리(정리 유도 포함) / 10분 전 알람 UI 노출 / 시작 전 목표 체크 페이지 동작 / 백엔드 pytest + web e2e 기존 스위트 pass / SSOT 대시보드 갱신.
## P2. 회기 계획·달성도 게이지 (설계 → 구현)
**요구사항(회의 방향)**:
1. 단계별 **누적 게이지**: 예) 라포 100 중 이번 회기 30 달성 — 회기가 이어질수록 누적. (초심 상담자는 라포를 1회기에 못 만드는 게 정상이라는 학습 신호)
2. 페르소나 수치값(신뢰/개방도)은 **전 회기 공유** — 장기 메모리 누적(기존 구현)과 정합.
3. 별도 "치료 계획 탭"은 만들지 않는다(복잡도 우려) — **계획서·프로토콜 파일 첨부 시 자동 반영**이 절충안. 치료계획은 페르소나에 장기/단기 사례 단서로 흡수.
4. 회기 간 망각 기능: **불필요로 결론** (교육 목적) — 작업 없음.
**진행 방식**: 설계 문서(1p) 먼저 → 소유자 확인 → 구현. P1과 독립적으로 진행 가능하나 P1 이후 권장(종료 조건 변경이 게이지 표시에 영향).
## P3. 파일럿 인프라 대비 (7월 말 전)
1. **20명 동시 사용 부하 검증** — 세션 동시 생성 시 RAG warm 동시성 이슈 이력 있음. 20 동시 세션 시나리오 부하 테스트 + 병목 기록.
2. **사용 지침·규정 문서** — 파일럿 기간 집중 트래픽 대비 사용 가이드(비교 점수 설계는 연구팀과 협의).
3. **서버 이관 대비** — 현 윤찬 자택 개인 서버 → 학교 정보처 호스팅/도메인 이관 협의 예정(협의 주체는 연구팀). 개발 몫: Docker Compose 스택의 호스트 이식성 점검(하드코딩 경로·도메인·시크릿 주입 방식), 이관 절차 문서 초안.
4. 비용 참고: 실사용 지난달 ~$10, 20명 액티브 시 최대 $100 이내 추정(연구팀이 서버·자문 비용 지원).
## P4. 데이터 업로드 흐름 검증 (즉시)
연구팀이 단톡방 자료를 교수자 페이지에 **자유 양식 엑셀(파란 라벨 방식 그대로)** 업로드 시작함("즉시" 합의). 개발 몫:
- 자유 양식 엑셀 업로드 → DB 적재 → AI용 변환 → **원본 파기** 흐름이 실데이터로 정상 동작하는지 검증.
- 실패 케이스(양식 변형·인코딩) 방어 확인. 원본 추고록은 학교 재산 — DB 원본 미적재·암호화 원칙 유지.
## P5. 협업 체계 (연구팀 직접 참여 개방)
1. **저장소 초대** — 소유자(윤찬) 직접 실행. 연구팀 디자인 담당 학생 포함.
2. **PR/브랜치 협업 가이드** 문서 작성 — 비개발자(연구팀·학생) 눈높이: 브랜치 생성→수정→PR→리뷰 흐름, 직접 main 덮어쓰기 금지 이유. `docs/guides/`에 배치.
3. **UI 문구 수정 프로세스** — 연구팀이 문구 수정 목록을 단톡방으로 전달 → 즉시 반영. 반영 작업은 단순 텍스트 치환 수준으로 유지.
## 외부 대기 (연구팀 몫 — 구현 착수 금지, 수신 시 반영)
| 항목 | 내용 |
|---|---|
| '실패(게임 오버)' 정의 | 어떤 발화가 실패이고 무엇이 표시돼야 하는지 — 연구팀 정의 후 구현 |
| 검수 표준화 항목 | 페르소나·AI 피드백 검수 항목(임상=연구팀 설계, 기술=개발 지원). 논문·상품화 validation 근거 |
| AI 평가 노출 정책 | 세션 종료 후에만 학생 노출 중 — 노출 범위·시점 정책 확정 대기 |
| 페르소나 5~6개 추가 | 연구팀 제작(추고록+프로토콜 쌍, 다음 주까지) — 개발 몫 없음 |
## 명시적 비(非)작업
- 비언어(표정·제스처) 레이블링·채점 — **차기 사업으로 분리** (이번 범위 아님).
- 회기 간 망각 기능 — 불필요 결론.
- 치료 계획 별도 탭 — 만들지 않음 (P2 절충안으로 흡수).

View file

@ -9,7 +9,7 @@
## 판단
페르소나 저작은 교수 콘솔 안의 보조 카드가 아니라 별도 작업면이어야 한다. 이 기능의 본질은 JSON 입력이 아니라, 실제 기록·교재·가이드 자료를 SSOT로 등록하고, RAG가 추출한 근거를 따라 AI 초안을 만들고, 교수자가 임상·회기·말투·안전·프롬프트 계약을 검수하는 저작 워크플로다.
페르소나 영역은 교수 콘솔 안의 보조 카드가 아니라 별도 운영·저작 작업면이어야 한다. 이 기능의 본질은 JSON 입력이 아니라, 운영 중인 페르소나의 학습·검수 상태를 파악하고 실제 기록·교재·가이드 자료를 SSOT로 등록한 뒤, RAG가 추출한 근거를 따라 AI 초안을 만들고 교수자가 임상·회기·말투·안전·프롬프트 계약을 검수하는 전체 생명주기다.
현재 캡처에서 문제였던 지점:
@ -33,10 +33,12 @@
1. 홈/대시보드는 역할별 작업대다. 학습자는 "다음 연습", 교수자는 "검토할 회기와 위험 신호", 관리자는 "운영 이상과 접근 권한"을 첫 화면에서 바로 처리해야 한다.
2. 한 화면에는 하나의 1차 CTA만 둔다. 나머지는 행 액션, 세컨더리 버튼, 필터로 낮춘다.
3. 카드 남발을 피한다. 반복 비교가 필요한 정보는 큐/테이블로, 개별 상세만 카드로 둔다.
4. PersonaStudio는 3열을 유지하되 의미를 고정한다.
- 좌측: 자료 큐, 저작 단계, 검수/공개 카탈로그
- 중앙: RAG 자료 등록, 탭별 구조화 편집, 저장/검수 요청
- 우측: 검증, 생성 근거, 마스킹, 승인/반려
4. PersonaStudio의 현행 정보구조는 상단 horizontal tab을 단일 진입점으로 삼는다.
- 대시보드: 공개/활성 페르소나, 학습 인원·세션, 실제 평가·라포 진척, 검수 요약
- 카탈로그: 카탈로그 정의, 승인본 공개 규칙, 이론 분포, 버전·출처
- 페르소나: 전체 table, 시스템/사용자 생성 구분, 공개/검수/숨김 상태, 상세·수정 drilldown
- 검수 현황: 페르소나 탭 내부 2차 horizontal tab으로 두고 승인/반려/수정 작업을 모은다.
- 신규/수정: 자료→초안→설정→검토 4단계 독립 작업면. 중앙 편집과 우측 검증/근거의 2열을 사용한다.
5. RAG 첨부 파일은 SSOT다. 원문 파일, 마스킹 결과, source/document/chunk hash, 초안 생성 evidence를 한 작업면에서 추적해야 한다.
6. 임상·회기·안전 탭은 빈 입력란만 두지 않는다. 각 탭 상단에 작성 목적, 포함할 정보, 피해야 할 입력을 짧게 제시한다.

View file

@ -0,0 +1,69 @@
# 파일럿 사용 지침 (초안) — 대학원생 참가자용
> **초안 — 연구팀 확정 전.** 접속 방법·연락 채널·세부 문구는 연구팀 확정 후 갱신한다.
> 대상: 효과성 검증 파일럿 참가자(대학원생 약 20명, 집중 사용).
> 근거: [`hanshin-meeting-actions-2026-07-13.md`](./hanshin-meeting-actions-2026-07-13.md) P3-2, 이번에 반영된 시간 기반 회기 흐름(P1).
이 도구는 **AI 내담자와 상담 회기를 연습**하고, 회기 중·후에 피드백을 받는 훈련 플랫폼입니다. 실제 내담자가 아니며, 연습 기록은 연구·교육 목적으로 사용됩니다.
---
## 1. 접속과 로그인
1. 안내받은 주소로 접속합니다(파일럿 도메인 — 연구팀 공지).
2. **Google 계정으로 로그인**합니다. 참가자 명단에 등록된 이메일(학교/승인 도메인)로 로그인하세요.
3. 처음 로그인하면 **"승인 대기"** 화면이 나올 수 있습니다.
- 운영자가 수업·연구 참여 범위를 확인하는 동안 화면이 열리지 않습니다.
- 승인되면 화면의 **"승인 상태 새로고침"** 버튼을 누르거나 다시 접속하면 온보딩으로 넘어갑니다.
- 반나절 이상 승인이 안 되면 아래 §6 연락 채널로 이메일을 알려주세요.
## 2. 온보딩과 동의
- 첫 진입 시 **온보딩·동의 화면**이 뜹니다. 도구의 목적, 데이터 사용 범위, 유의사항을 읽고 동의해야 회기를 시작할 수 있습니다.
- 동의는 언제든 철회 가능합니다(철회 절차는 연구팀 안내를 따름).
## 3. 회기 진행 방법 (새 흐름)
이번 파일럿부터 회기는 **"단계 완수"가 아니라 "시간"으로 끝납니다.** 실제 상담처럼 한 회기에 모든 걸 끝내지 않아도 됩니다.
1. **회기 준비 페이지 — 목표 선택**
- 회기를 시작하기 전, 이번 회기에서 집중할 **목표를 1~4개 선택**합니다(라포/탐색/개입/정리 중 — 2개 수준 권장).
- 4개를 다 하려 하지 마세요. 초심 상담자가 한 회기에 라포만 다지는 것도 정상입니다.
2. **회기 진행 — 약 60분(시간 기반)**
- 목표 달성 여부와 관계없이 **시간이 되면 회기가 종료**됩니다.
- AI 응답에 약간의 지연이 있어 실제 대화 시간은 40~50분 정도입니다.
- 목표를 일찍 이뤄도 시간 안에서는 계속 대화를 이어갈 수 있습니다.
3. **종료 10분 전 알람**
- 종료 10분 전에 화면에 알람이 뜹니다. 이때부터 **대화를 정리(마무리)** 하는 방향으로 이끌어 주세요.
- 마무리 인사를 나눈 뒤에는 시간 전이라도 자연스럽게 회기가 조기 종료될 수 있습니다.
4. **회기 종료 후 — 리뷰 확인**
- 회기가 끝나면 **리뷰(회기 요약·피드백)** 를 확인할 수 있습니다.
- 단계별 진행은 **누적**됩니다. 이번 회기에 라포를 조금 쌓았다면 다음 회기에 이어집니다(회기 간 페르소나 신뢰/개방도도 이어짐).
## 4. 라이브 코칭 사용법
회기 **중에** 짧은 슈퍼비전 힌트를 받을 수 있는 기능입니다.
- 화면의 **코칭 탭**에서 확인합니다. 상담 흐름을 끊지 않도록, 턴을 주고받은 뒤 다음 한 문장을 더 낫게 만드는 짧은 조언(다음 발화 제안 포함)이 나옵니다.
- **기회는 회기당 3개**로 시작합니다. 코칭을 한 번 받을 때마다 1개씩 소모됩니다.
- **충전(다시 채워지는) 규칙**:
- **좋은 발화**로 내담자에게 긍정적 변화 신호(신뢰·개방도 상승)가 확인되면 1개 충전, 또는
- **6턴마다** 자동으로 1개 충전(최대 3개까지).
- 그래서 잘 진행할수록, 또 오래 진행할수록 코칭 기회가 다시 생깁니다. 아껴 쓰되 막히는 순간에 활용하세요.
- 참고: 코칭은 **점수·정답·내담자 내부 설정을 알려주지 않습니다.** 관찰 가능한 상담 행동과 다음 발화 방향만 제시합니다. AI 엔진이 일시적으로 느릴 때는 규칙 기반의 간단한 코칭으로 대체되어 계속 동작합니다.
## 5. 유의사항 (꼭 읽어주세요)
- **개인정보를 입력하지 마세요.** 외부 AI로 보내기 전 이름·연락처 등은 **자동 마스킹**되지만, 실명·전화번호·주소·기관명 등은 **처음부터 입력을 자제**하세요. 연습에는 필요하지 않습니다.
- **실제 사례의 식별 정보를 넣지 마세요.** 연습용 가상의 상황으로 진행하세요.
- **위기 문구 관련 안전 게이트**: 대화 중 자·타해 등 위기 단서가 감지되면, 시스템은 방법을 캐묻지 않고 **안전 확인·보호요인 점검·기관 연결** 방향으로 코칭을 우선합니다. 이는 정상 동작이며 훈련의 일부입니다. 실제 위기 상황이라면 이 도구가 아니라 정식 상담·응급 기관(예: 자살예방상담 109)에 연락하세요.
- 이 도구는 **훈련용 시뮬레이션**입니다. AI 내담자의 반응은 학습 목적으로 설계된 것이며 실제 임상 판단의 근거로 삼지 마세요.
## 6. 문제 발생 시 연락
- 로그인/승인 지연, 화면 오류, 회기가 뜨지 않음, 코칭이 계속 안 나옴 등의 문제는 **연구팀 지정 채널(단톡방/이메일 — 연구팀 확정)** 로 알려주세요.
- 신고 시 도움이 되는 정보: 로그인 이메일, 발생 시각, 회기 ID(리뷰 화면에서 확인 가능), 화면 캡처.
---
> 이 문서는 초안입니다. 접속 주소, 연락 채널, 동의 문구, 비교 점수 노출 정책 등은 연구팀 확정 후 갱신합니다.

View file

@ -0,0 +1,95 @@
# 전 저장소 리팩터 거버넌스 실행 기록 — 2026-07-15
> 상태: **DONE (증거 기반 구조 개선 패스)**
> 방법: `refactor-governance` Edit Pass(P1→P8), 동작 보존 우선
> 권위 상태: `docs/dev_dashboard.html`과 동기화. 기능 로드맵이나 외부 실증 게이트를 대신하지 않는다.
## 1. 목표와 불변 조건
목표는 파일 길이를 기계적으로 줄이는 것이 아니라, 변경 시 서로 어긋날 수 있는 계약·권한·캐시·표시 규칙을 한 소유자로 모으고 반복 IO와 초기 번들 비용을 줄이는 것이었다.
불변 조건:
- 인증/RBAC/RLS, 감사 로그, PII 마스킹, durable/degraded 판정은 바꾸지 않는다.
- API 응답과 생성 OpenAPI 타입, 엔진 packet, DB 스키마의 의미를 바꾸지 않는다.
- 세션 턴 순서, 평가 재시도, 알림 수신자, 화면의 오류 문구와 빈 상태를 보존한다.
- 기존 사용자 작업이 섞인 dirty worktree를 되돌리거나 일괄 포맷하지 않는다.
- 공통화는 오류 순서·캐시 fallback·side effect가 같은 경우에만 한다.
## 2. 감사 범위와 최초 증거
아카이브를 제외한 생산 코드, 테스트, 인프라, 활성 문서를 전수 스캔했다.
| 영역 | 파일/줄 기준 | 중점 검사 |
|---|---:|---|
| `apps/api/app` | 100 files / 45,112 lines | 장기 함수·인자 묶음·SQL 반복·캐시·LLM 감사·세션 IO |
| `apps/web/src` | 74 files / 46,935 lines | 대형 라우트·중복 view model·API 타입·초기 번들·디자인 소유권 |
| `apps/web/e2e` | 25 files / 12,381 lines | 동작/레이아웃/DB 증거 게이트 |
| `scripts`, `infra` | 37 files / 7,430 lines | 런타임 스키마와 운영 경로 중복 |
| 활성 `docs`, `data` | 111 files / 42,955 lines | SSOT drift·검증 숫자·현재/아카이브 경계 |
최초 정적 결과는 Ruff 16건, 생산 코드 중복 15 clones / 327 lines(0.60%), 초기 JS 678.88 kB(gzip 195.75 kB), 초기 CSS 328.49 kB(gzip 51.46 kB)였다. Python 함수 1,903개 중 60줄 이상 135개·인자 7개 이상 26개, TS/TSX 함수 1,426개 중 60줄 이상 61개가 후보였다. 이 후보는 길이만으로 수정하지 않고 호출·중복·소유권 증거를 다시 확인했다.
## 3. 적용한 패치 그룹
| 그룹 | 변경 | 단일 소유자/효과 |
|---|---|---|
| P1 삭제·정적 정리 | Ruff 16건 제거, 미사용 컴포넌트·의존성·죽은 export 정리, Knip 게이트 도입 | 참조 0인 코드와 선언 drift 제거 |
| P2 shape 안정화 | dataset manifest, voice context/prosody/turn/audio를 명시적 input object로 전환 | 긴 positional/keyword 묶음의 의미를 타입 이름으로 고정 |
| P3 계약 SSOT | 평가 write 계약, OpenAPI 생성 타입, `runtime_schema.py`, format/runtime diagnostics를 단일화 | API/DB/runtime/UI 미러 선언 drift 차단 |
| P4 캐시 | KB process cache의 key·수명·invalidate 경로를 한 모듈에 고정 | admin sync 뒤 stale source pack 방지 |
| P5 workflow | KB/persona source workflow, LLM generate+audit, 알림 수신자, managed session sync, admin normalization 공통화 | 같은 side effect·오류 의미를 한 구현으로 통합 |
| P6 성능 | `App.tsx` 전 라우트 lazy loading, 공통 Suspense, 세션 상태/턴 batch read | 초기 payload 감소, missing-evaluation 복구 1+2N query를 3 query로 축소 |
| P7 인프라 | dev runtime schema와 `infra/db/init` 정의 정합, prod DDL fail-closed, E2E fixture/single-run 2단계 실행 | 개발 자동보강과 운영 migration 역할 분리, 단일 DB/engine 포화 방지 |
| P8 UI/디자인 | Surface/AppShell/theme/페르소나 시각 view model SSOT, 대형 화면의 순수 모델·음성 캡처 분리, raw-color 예산 게이트 | 공통 primitive와 화면 예외의 소유권을 테스트 가능한 규칙으로 고정 |
추출된 주요 경계:
- 백엔드: `services/llm_audit.py`, `runtime_schema.py`, 평가/세션/알림/KB의 named contract와 batch loader.
- 프론트: `lib/personaViewModel.ts`, `lib/runtimeDiagnostics.ts`, `pages/admin/dataNormalization.ts`, `pages/persona-studio/model.ts`, `pages/learner-home/model.ts`, `pages/session-review/model.ts`, `pages/session/voiceCapture.ts`.
- 거버넌스: `check:dead-code`, `check:duplication`, `check:design-ssot`, `check:api-types`.
## 4. 전후 측정
| 지표 | 이전 | 현재 | 판정 |
|---|---:|---:|---|
| 생산 코드 중복 | 15 clones / 327 lines / 0.60% | 1 clone / 15 lines / 0.03% | 95.4% duplicated-line 감소, threshold 0.05% 게이트 |
| 초기 JS | 678.88 kB / gzip 195.75 kB | 241.49 kB / gzip 77.13 kB | gzip 60.6% 감소 |
| 초기 CSS | 328.49 kB / gzip 51.46 kB | 28.59 kB / gzip 6.45 kB | gzip 87.5% 감소 |
| `Session.tsx` | 4,002 lines | 3,491 lines + `voiceCapture.ts` | 음성 브라우저 경계 분리 |
| `LearnerHome.tsx` | 2,053 lines | 1,857 lines + 222-line model | 표시 계산을 순수 모델로 분리 |
| API 단위 테스트 | 393 | 400 passed | 신규 구조 회귀 포함 |
| Gateway 단위 테스트 | 27 | 29 passed | packet/model 계약 포함 |
| Playwright 수집 | 214 / 20 files | 215 / 20 files | 현행 목록 동기화 |
## 5. 의도적 비추출과 통제된 예외
1. `session_persistence.py`의 15-line clone 한 건은 유지한다. 두 principal-aware acquire 경로는 표면 구조만 같고 예외 시 cache fallback과 durable 판정이 다르다. 합치면 실패 의미가 숨겨지므로 `jscpd` 전체 threshold 안의 근거 있는 제외다.
2. `Session`, `Admin`, `PersonaStudio`, `Professor`의 라우트 컨테이너는 여전히 크지만 현재 생산 TS/TSX 중복은 0이다. 화면별 상태 전이까지 무리하게 generic hook/component로 만들면 읽기 비용과 prop surface가 늘어난다. 새 기능이 독립 상태·독립 E2E를 가질 때 해당 slice를 추출한다.
3. 세션 dark stage와 아바타/인증 아트의 국소 raw color는 전역 토큰으로 승격하지 않는다. 공통 `ui.css`/`shell.css`는 raw color 0개를 강제하고, 예외 파일은 2026-07-15 기준 수치 이상 증가하지 못하도록 고정 예산을 둔다.
4. 전체 215개 Playwright에는 실제 DB·엔진·provider가 필요한 single-run 시나리오가 섞인다. 기존 direct runner는 로컬에서 12 workers로 세 프로젝트를 동시에 실행해 188 passed/27 resource-timeout을 만들었고, single-run을 뺀 12-worker fixture 단계도 145/166 뒤 21 request timeout을 재현했다. `npm run e2e`를 fixture desktop/mobile `workers=4` 단계 뒤 DB/engine `workers=1` 단계가 시작되는 구조로 바꿨다.
5. 관리자 티켓 큐는 최대 120개 복합 카드를 한 번에 다시 그리며 액션 pending 상태까지 페이지 루트가 소유했다. 카드 렌더 상한을 24개로 두고 나머지는 서버 검색/필터로 탐색하게 했으며, 중복 요청 잠금과 시각 pending은 `TicketActions`가 카드 단위로 소유한다. 네이티브 입력 dispatch와 React 상태 렌더를 분리해 해결/중복 연결 실제 클릭 4건이 desktop/mobile에서 통과한다.
6. Settings 초기 로드는 React 개발 모드 effect 재실행으로 같은 GET 두 개가 경합할 수 있었고, 늦게 끝난 응답이 사용자가 방금 바꾼 엔진 모드·모델을 원래 값으로 덮어썼다. 요청 세대 번호를 추가해 최신 로드만 상태를 반영하고 unmount된 요청은 무효화했다. 기존 engine settings E2E가 실제 PATCH body와 복구까지 고정한다.
7. AI 튜터 DB 영속화는 정상 provider 응답이 독립 실행에서도 138.5초 걸려 기존 150초 테스트 상한과 여유가 11초뿐이었다. 기능 timeout을 숨기지 않고 동일 파일의 장시간 실엔진 기준인 240초로 테스트 예산을 조정했으며, 최종 전체 직렬 실행에서는 2.1분에 ready 응답·source pack·DB history·reload 증거를 모두 통과했다.
## 6. 검증 증거
- `ruff check app` — passed.
- `pytest -q app`**400 passed**, Starlette `python_multipart` PendingDeprecationWarning 1건.
- `pytest -q engine_gateway`**29 passed**, 같은 외부 의존 warning 1건.
- `npm run typecheck` / `check:api-types` / `check:design-ssot` / `check:dead-code` / `check:duplication` — passed.
- `npm run build` — 109 modules, initial JS 241.49 kB(gzip 77.13 kB), initial CSS 28.59 kB(gzip 6.45 kB).
- `npm audit --audit-level=high` — 0 vulnerabilities.
- `npx playwright test --list`**215 tests / 20 files**.
- `npm run e2e:parallel`**166 passed** (desktop 83 + mobile 83, workers=4).
- `npm run e2e:single-run`**49 passed** (DB/engine/provider 직렬, 23.2분).
- 전체 Playwright 최종 판정 — **215 / 215 passed**.
- `git diff --check` — passed; checkout 정책에 따른 LF→CRLF 경고만 존재.
## 7. 앞으로의 변경 규칙
- 새 DTO/packet은 OpenAPI·schema·named adapter 중 한 곳만 원본으로 둔다.
- 새 캐시는 key, SoR, invalidate, TTL, 실패 시 durable/degraded 의미를 함께 정의한다.
- 새 공통화는 `check:duplication`의 실제 clone 또는 둘 이상의 동일 side effect가 증거일 때만 한다.
- 새 페이지 색은 먼저 의미 토큰을 사용하고, 국소 예외면 이유와 raw-color 예산을 함께 갱신한다.
- 라우트 컨테이너 추출은 독립 입력/출력, 독립 상태, 독립 테스트가 생긴 뒤 한다. 파일 길이만으로 분리하지 않는다.

View file

@ -0,0 +1,191 @@
# 서버 이관 런북 (초안) — 자택 개인 서버 → 학교 정보처 호스팅
> **초안 — 연구팀·정보처 협의 후 확정.** 이 문서는 개발(트웬티온스) 몫인 *기술 이관 절차* 초안이다.
> 도메인·서버 사양·비용·보안 정책의 최종 결정 주체는 연구팀·한신대 정보처이며, 아래 값과 절차는 협의 결과에 맞춰 갱신한다.
> 스펙 근거: [`hanshin-meeting-actions-2026-07-13.md`](./hanshin-meeting-actions-2026-07-13.md) P3.
## 0. 요약 (한 장)
- **현재 운영 형태**: 윤찬 자택 Windows PC에서 `scripts/start-public-runtime.ps1`**베어 프로세스**(uvicorn + `vite preview` + 로컬 `claude -p` 게이트웨이)를 띄우고, **Cloudflare 터널**로 공개하며, **Windows 작업 스케줄러 워치독**이 살아있게 유지한다. → 이 경로 전체가 Windows 종속이라 리눅스로 그대로 못 옮긴다.
- **이관 목표 형태**: 학교 리눅스 서버에서 **`infra/docker-compose.yml` 스택**(web·api·db·proxy)을 그대로 기동. Docker 스택은 이식성을 전제로 설계돼 있어 Windows 런타임 스크립트 계층을 대부분 대체한다.
- **가장 큰 이관 블로커 2개**: ① 엔진 모드 `claude_cli`(개인 구독 CLI 인증) → `claude_api`(`ANTHROPIC_API_KEY`) 전환 필수. ② 하드코딩된 도메인/리다이렉트 URI/허용 호스트를 학교 도메인으로 교체(일부는 **빌드 시점**에 굽혀서 재빌드 필요).
---
## 1. 사전 요구사항 (이관 시작 전 확보)
### 1.1 정보처가 준비할 것
- **리눅스 호스트**: Docker Engine + Docker Compose v2 설치 가능한 서버(Ubuntu 22.04 LTS 등). 20명 파일럿 기준 최소 4 vCPU / 8GB RAM / 40GB SSD 권장(부하 검증 결과에 맞춰 조정).
- **공개 도메인 2개**: 웹용 1개 + API용 1개(예: `vignette.hs.ac.kr`, `api-vignette.hs.ac.kr`). **웹과 API는 별도 호스트네임**이어야 한다(OAuth 콜백·CORS 설계 전제).
- **DNS A/AAAA 레코드**: 위 두 도메인이 서버 공인 IP로 향하도록 설정.
- **TLS**: 다음 중 하나.
- (a) 서버에 공인 IP가 있으면 **Caddy 자동 TLS**(Let's Encrypt) 사용 — `.env``SITE_ADDRESS`, `ACME_EMAIL`만 채우면 됨.
- (b) 정보처가 기관 인증서를 강제하면 Caddy 앞단에 리버스 프록시를 두거나 인증서를 Caddy에 주입(협의 필요).
- **아웃바운드 네트워크 허용**: `api.openai.com`, `api.anthropic.com`(엔진 claude_api 모드), Google OAuth 엔드포인트로의 HTTPS egress.
### 1.2 연구팀·개발이 준비할 것
- **Google OAuth 클라이언트**: Google Cloud Console에서 **Authorized redirect URI에 `https://api-<학교도메인>/auth/callback` 추가**(기존 chanpaca/18ka URI는 유지 또는 정리). Client ID/Secret 확보.
- **API 키**: `OPENAI_API_KEY`(음성 STT/TTS), `ANTHROPIC_API_KEY`(엔진 claude_api 모드 — 아래 §2 참조).
- **관리자·교수자 이메일 목록**: `AUTH_SUPER_ADMIN_EMAILS`, `AUTH_ADMIN_EMAILS`, `AUTH_TEACHER_EMAILS`, 허용 도메인 `AUTH_ALLOWED_EMAIL_DOMAINS`(파일럿 참가자 도메인 포함).
- **현재 운영 데이터 스냅샷**: DB 덤프 + uploads 디렉터리(아래 §4).
---
## 2. 엔진 모드 주의 — claude_cli → claude_api (필수)
현재 운영은 `ENGINE_MODE=claude_cli`다. 이는 **호스트에 상주하는 개인 `claude` CLI(개인 구독 OAuth 인증)** 를 게이트웨이(`http://host.docker.internal:9099`)로 호출하는 방식이라 **학교 서버에는 옮길 수 없다**(개인 계정 인증에 묶임).
학교 서버에서는 반드시 다음으로 전환한다.
```dotenv
ENGINE_MODE=claude_api
ANTHROPIC_API_KEY=sk-ant-... # 연구팀/기관 발급 키
# ENGINE_URL 은 claude_api 모드에서 사용하지 않음 (host.docker.internal 라인 무시됨)
```
- 코드는 `claude_api`를 **기본값으로 지원**한다(`apps/api/app/config.py``engine_mode` 기본이 `claude_api`). 별도 개발 없이 env 전환으로 동작한다.
- 비용: 회의 기록상 실사용 지난달 ~$10, 20명 액티브 시 최대 $100 이내 추정. 과금은 `ANTHROPIC_API_KEY` 계정으로 발생 → 연구팀/기관 계정 사용 권장. 관리자 UI에서 `ADMIN_USAGE_BUDGET_USD`로 예산 표시 가능.
- `claude_cli` 게이트웨이(`apps/api/engine_gateway/`), 로컬 `claude -p` 상주풀, `ENGINE_READY_TTL_SECONDS` 튜닝은 **모두 불필요**해진다.
---
## 3. 이관 단계
### 3.1 코드 배치 + 환경파일 작성
```bash
git clone <저장소> vignette && cd vignette/infra
cp .env.example .env
# .env 편집 — 아래 §5 체크리스트의 "설정만 바꾸면 됨" 값을 학교 값으로 교체
```
배포 전 프리플라이트(읽기 전용 검증):
```bash
# 저장소 루트에서
python scripts/check-deploy-preflight.py --env-file infra/.env
# DB 앱롤까지 검증하려면 app-role DATABASE_URL 을 넘기고 --require-app-role
```
이 스크립트는 필수 env 누락, `change-me` 류 플레이스홀더, prod에서 켜지면 안 되는 플래그(`AUTH_DEV_LOGIN_ENABLED` 등), 잘못된 `ENGINE_MODE`를 잡아준다.
### 3.2 DB dump / restore
현재 DB(자택)에서 덤프를 뜬다. 접속 방식에 맞춰 하나 선택.
```bash
# (A) DB가 docker compose db 컨테이너인 경우 — 자택 서버에서
docker compose exec -T db pg_dump -U vignette_owner -Fc vignette > vignette-$(date +%F).dump
# (B) DB가 외부/NAS PostgreSQL인 경우 — 접속 문자열로 직접
pg_dump --format=custom --no-owner --no-privileges \
--dbname="postgresql://vignette_owner:<PW>@<OLD_HOST>:5432/vignette" \
--file=vignette-$(date +%F).dump
```
학교 서버에서 복원. **권장: 스키마 init 스크립트가 만든 빈 DB가 아니라, 완전히 빈 DB에 풀 덤프를 복원**한다(스키마·데이터·RLS 정책을 덤프가 통째로 재현하도록).
```bash
# 새 호스트: db 컨테이너만 먼저 띄우되 init 스크립트와 충돌을 피하려면
# - 신규 빈 볼륨 + init 스크립트로 스키마 생성 후 --data-only 복원, 또는
# - init 없이 빈 DB 생성 후 풀 덤프 복원(권장, 아래)
docker compose up -d db
docker compose exec -T db psql -U vignette_owner -c "CREATE DATABASE vignette_new;"
cat vignette-YYYY-MM-DD.dump | docker compose exec -T db \
pg_restore --no-owner --no-privileges --clean --if-exists \
-U vignette_owner -d vignette_new
# 검증 후 vignette_new 를 정식 DB로 승격(또는 처음부터 vignette 로 복원)
```
> ⚠️ **RLS/앱롤 주의**: 스키마는 `infra/db/init/``01_extensions.sql … 06_session_evaluation.sql`**빈 볼륨 최초 기동 시 1회만** 실행한다. 여기엔 RLS 정책(`04_audit_eval_rls.sql`)과 앱 전용 롤 생성(`99_app_role.sh`)이 포함된다. 풀 덤프 복원 시 롤/권한이 덤프에 없으면(=`--no-owner --no-privileges` 사용) **복원 후 `99_app_role.sh` 상당의 앱롤 GRANT를 다시 적용**해야 API app-role(`vignette_app`)이 붙는다. 프리플라이트 `--require-app-role`로 확인.
### 3.3 persona / KB 데이터 이동
페르소나·평가·라이브코칭 KB는 **저장소가 원본을 소유**하고 스크립트로 DB에 동기화한다. DB 덤프에 이미 승인된 `persona_card` 행이 포함되지만, KB 소스팩은 코드 기준으로 재동기화한다.
```bash
# dry-run(기본): repo 소스팩 해시 vs 활성 kb.document 비교
python scripts/sync-persona-sources.py
# 실제 반영
python scripts/sync-persona-sources.py --apply
```
- 페르소나 시드 재생성이 필요하면 `python scripts/materialize-persona-seeds.py`.
- 원칙: prod에서는 `AUTO_SEED_PERSONAS=false`, `ALLOW_SEED_PERSONA_FALLBACK=false` 유지(승인된 DB 행만 사용). 추고록 원본 등 학교 재산 데이터는 repo·이미지에 넣지 않는다.
### 3.4 uploads 디렉터리 이동
사용자 업로드(교수자 자료 등)는 `apiuploads` 도커 볼륨(`/app/uploads`, `USER_UPLOAD_DIR`)에 있다.
```bash
# 자택: 볼륨 내용 tar 로 추출
docker run --rm -v vignette_apiuploads:/data -v "$PWD":/backup alpine \
tar czf /backup/uploads-$(date +%F).tgz -C /data .
# 학교: 새 볼륨에 복원
docker run --rm -v vignette_apiuploads:/data -v "$PWD":/backup alpine \
tar xzf /backup/uploads-YYYY-MM-DD.tgz -C /data
```
### 3.5 기동
```bash
cd infra
docker compose build # 웹은 도메인이 빌드에 굽히므로 §5 빌드 인자 확인 후 빌드
docker compose up -d
docker compose ps # web/api/db/proxy healthy 확인
```
---
## 4. 이식성 이슈 체크리스트
### 4.1 설정만 바꾸면 되는 것 (env / DNS / 콘솔 — 재빌드·코드수정 불필요)
| 항목 | 현재 하드코딩/기본값 | 이관 시 조치 |
|---|---|---|
| API 리다이렉트 URI | `OAUTH_REDIRECT_URI=https://api-vignette.chanpaca.net/auth/callback` | 학교 API 도메인으로 교체 + Google Console에 등록 |
| 프론트 기준 URL | `FRONTEND_BASE_URL=https://vignette.chanpaca.net` | 학교 웹 도메인 |
| 콜백→프론트 매핑 | `FRONTEND_ORIGIN_MAP={api-vignette.chanpaca.net:…, api-vnet.18ka.net:…}` | 학교 `api-호스트: https://웹-호스트` |
| CORS 허용 | `CORS_ORIGINS=[chanpaca.net, 18ka.net, pages.dev, localhost…]` | 학교 웹 도메인만(로컬 항목 제거) |
| 슈퍼관리자 | `AUTH_SUPER_ADMIN_EMAILS=[yunchan@twentyoz.kr, hoonjungkoo@hs.ac.kr]` | 운영 담당자 이메일 |
| 허용 도메인 | `AUTH_ALLOWED_EMAIL_DOMAINS=[hs.ac.kr, twentyoz.kr]` | 파일럿 참가 도메인 |
| 시크릿 | `POSTGRES_PASSWORD/APP_DB_PASSWORD/SESSION_SECRET`(=`change-me…`) | 랜덤 강값으로 교체 |
| TLS/노출 | `SITE_ADDRESS=:80`, `ACME_EMAIL=`(빈값) | 학교 웹 도메인 + 관리자 메일 |
| 포트 | `HTTP_PORT=8080`, `HTTPS_PORT=8443` | 정보처 방화벽 정책에 맞춰 |
| 엔진 | `ENGINE_MODE=claude_cli` | `claude_api` + `ANTHROPIC_API_KEY`(§2) |
### 4.2 고쳐야(재빌드/치환해야) 넘어가는 것
| 항목 | 위치 | 왜 블로커인가 | 조치 |
|---|---|---|---|
| **웹 허용 호스트** | `apps/web/vite.config.ts` `defaultAllowedHosts`(chanpaca/18ka/ts.net 배열) | `vite preview`가 Host 헤더를 검증 → 학교 도메인 누락 시 전체 화면이 403 블랭크. **빌드 시점 값**이라 재빌드 필요 | 빌드 전 `VITE_ALLOWED_HOSTS=<학교 웹 도메인>` 환경변수로 주입(코드 수정 없이 가능) |
| **웹 API 베이스** | compose `web` 빌드 인자 `VITE_API_BASE=${PUBLIC_API_BASE:-/api}` | 프론트가 붙는 API 경로가 **빌드에 굽힘** | Caddy 프록시로 `/api` 동일 오리진 유지 시 기본값 그대로 OK. 별도 API 도메인 직결이면 `PUBLIC_API_BASE` 지정 후 재빌드 |
| **엔진 인증 방식** | `ENGINE_MODE=claude_cli` 런타임 경로 | 개인 구독 CLI 인증 → 학교서버 이식 불가 | §2대로 `claude_api` 전환(코드 지원됨, env 전환) |
| **Windows 런타임 계층 전체** | `scripts/*.ps1`, `.vbs`, Windows 작업 스케줄러(`install-public-runtime-task.ps1`, `register-boot-task.ps1`), `Get-CimInstance`, winget 경로 | Windows 전용. `$Workspace="D:\workspace\vignette"`, `$Python=%LOCALAPPDATA%\…`, `$Cloudflared`, `$USERPROFILE\.cloudflared` 등 하드코딩 | 리눅스에서는 **docker compose 스택 + systemd/cron 헬스체크**로 대체(PowerShell 스크립트 미사용). Cloudflare 터널은 공인 IP+Caddy 자동TLS로 대체 권장 |
| **config.py 하드코딩 기본값** | `apps/api/app/config.py``oauth_redirect_uri/frontend_origin_map/cors_origins/auth_super_admin_emails` 기본값에 chanpaca·18ka·특정 이메일 | env로 override되지만, env 누락 시 자택 값으로 조용히 폴백될 위험 | env를 반드시 채우고(§4.1), 이관 확정 후 기본값도 중립화 검토(별도 PR) |
> **비밀 주입 방식**: 현재는 `infra/.env` **평문 파일** 한 장(요구변수는 compose `${VAR:?}` 문법으로 미설정 시 기동 실패 → 누락 방어는 됨). 학교 서버에서는 `.env` 파일 권한 `chmod 600` + repo 미포함 확인, 가능하면 정보처 시크릿 관리 수단으로 승격 협의. 시크릿 매니저/볼트는 현재 미사용.
---
## 5. 이관 후 검증 체크리스트
```bash
# 1) 헬스 — environment=prod, db=true, engine=true, engine_mode=claude_api 확인
curl -s https://api-<학교도메인>/health | jq
# 기대: {"status":"ok","environment":"prod","db":true,"engine":true,"engine_mode":"claude_api", ...}
# 2) dev-login 꺼짐 확인 — prod에서 AUTH_DEV_LOGIN_ENABLED=false 여야 함(preflight도 검증)
# /auth/dev-login 류가 비활성인지, 로그인은 Google OAuth로만 되는지 확인
# 3) OAuth 왕복 — Google 로그인 → 콜백 → pending 승인 대기 → 슈퍼관리자 승인 → 온보딩 진입
# 4) /turn 스모크 — 세션 1개 생성 후 발화 1턴 왕복(내담자 응답 + 라이브 코칭 카드 수신)
# 5) 백엔드 pytest
cd apps/api && python -m pytest -q
# 6) (선택) 20 동시 세션 부하 — scripts/load-test-sessions.py 로 병목 재확인
```
추가 확인:
- 음성 SSE/WSS 경로가 Caddy `flush_interval -1`로 버퍼링 없이 흐르는지(자막·실시간 피드백).
- persona 목록이 승인 DB 행 기준으로 뜨는지(시드 폴백 아님).
- uploads 업로드→변환→원본 파기 흐름(P4)이 실데이터로 동작하는지.
---
## 6. 롤백 / 주의
- 이관은 **자택 운영을 내리기 전에** 학교 서버에서 병렬로 세워 검증하고, 검증 통과 후 DNS를 전환한다(다운타임 최소화).
- DB 덤프는 **이관 시점 스냅샷**이다. 병렬 운영 중 자택에서 발생한 신규 데이터는 최종 컷오버 시점에 재덤프·재복원(또는 컷오버 동안 자택 쓰기 중지).
- OAuth redirect URI를 학교 도메인으로 바꾸는 순간 기존 도메인 로그인은 깨질 수 있으니, 컷오버 타이밍을 연구팀과 합의.
- 확정 후 이 문서의 "초안" 표기를 제거하고 [`DEPLOYMENT.md`](../DEPLOYMENT.md)·SSOT 대시보드에 반영한다.

View file

@ -0,0 +1,48 @@
# P2 회기 계획·달성도 게이지 설계 (1p)
> 2026-07-13 한신대 회의 P2 결정의 구현 설계. 소유자 확인용 1페이지.
> 원칙: 게이지는 **백엔드 결정론 상태머신 수치에서 파생**하며, LLM이 만들지 않는다.
> CCD·정답 라벨·ideation 등 내담자 내부 설정은 계속 비노출(M6). 게이지는 "훈련 진행 신호"만 보여준다.
## 1. 무엇을 보여주나
| 표면 | 내용 |
|---|---|
| 세션 화면(좌측 트랙) | 단계별 누적 게이지 4개 — "라포 100 중 65" 식. 이번 회기 목표 단계에 목표 배지(P1 구현됨) |
| 세션 화면(관찰 패널) | **상세 수치**: 유효 개방도(%), 방어(저항) 수준(%), 라포 누적(%) + 이번 회기 증가분(+N%p) |
| 학습자 홈(페르소나 카드) | 페르소나별 라포 누적 게이지 — 회기가 이어질수록 차오르는 학습 신호 |
**학습 신호 설계 의도(회의 합의)**: 초심 상담자는 라포를 1회기에 못 만드는 게 정상이다.
게이지가 회기를 건너 누적되는 모습 자체가 "여러 회기에 걸쳐 쌓는 것"임을 가르친다.
## 2. 게이지 공식 (결정론)
상태머신(`state_machine.py`)의 기존 수치만 사용한다. 새 상태 없음.
- 단계 전이 조건은 `turns_in_stage >= STAGE_MIN_TURNS[stage]` AND `rapport_credit >= STAGE_ADVANCE_RAPPORT[stage]`.
- **단계 게이지**(stage s):
- 지나온 단계: `100`
- 미래 단계: `0`
- 현재 단계: `min(99, rapport_credit / STAGE_ADVANCE_RAPPORT[s] × 99)` — 라포 누적이 게이지의 본질.
(두 게이트를 모두 충족해 전이 대기 상태면 99에서 대기, 전이 순간 100)
- `정리`(CLOSE)는 진입 자체가 100 (전이 임계 없음)
- **누적성**: `rapport_credit`은 회기 종료 시 ×0.7로 이월(`init_state(carry)`, 기존 구현)되므로
다음 회기 게이지는 0이 아니라 이월분에서 시작한다 → "누적 게이지"가 자동 성립.
- **이번 회기 증가분**: `rapport_credit prev_rapport_credit`(세션 생성 시 저장된 이월 기준값, 기존 컬럼).
- **방어(저항) 수준**: `resistance`(0~1)를 %로. 학습자 표기는 "방어 신호(저항)" — 학술 용어화.
- **유효 개방도**: `effective_openness`(이미 학습자 노출 중인 값)를 %로.
## 3. 계약 변경
- `session_read_model.py``SessionProgress` DTO 신설:
`stages[] {stage, percent, achieved, is_goal}` + `rapport_percent, rapport_delta_percent, resistance_percent, openness_percent`.
- 파생 함수는 순수함수 `build_session_progress(state, prev_rapport_credit, goal_stages)` — 상태머신 상수를 단일 원천으로 읽는다.
- 노출 지점: `SessionDetailResponse.progress`(새로고침 복원), `TurnResponse.progress` + 스트림 `done` payload(턴마다 갱신),
대시보드 `persona_progress[].rapport_percent`(홈 카드).
- **비노출 유지**: ideation_stage, CCD, 정답 라벨, raw resistance 원값 명칭("저항 엔진" 등 내부 용어).
## 4. 비(非)작업 (회의 결정 준수)
- 별도 "치료 계획 탭" 없음 — 계획서·프로토콜은 페르소나 RAG 첨부(`/personas/sources`, 기존 경로)로 흡수.
- 회기 간 망각 기능 없음.
- 페르소나 수치의 전 회기 공유는 기존 carry-over(라포 ×0.7, resistance drift)가 이미 소유 — 변경 없음.

View file

@ -0,0 +1,51 @@
# 연구팀 태깅 축어록 4종 활용 — 실행 기록 + 후속 계획 (2026-07-15)
> 원천: 2026-07-14 연구팀 제공 태깅 엑셀 4종(카카오톡 단톡방).
> 회의 근거: `ops/hanshin-meeting-actions-2026-07-13.md` P4(데이터 업로드)·페르소나 확충,
> 2026-07-14 정식 회의록("데이터가 많을수록 성능·비용·응답속도 개선 — 추가 데이터 지속 요청").
> 원본 파일은 저장소·DB에 적재하지 않는다(원본 파기 원칙). 실행 내역과 후속 계획만 이 문서가 소유한다.
## 1. 자료 개요
| 자료 | 내용 | 태그 규모 | 페르소나 산출 |
|---|---|---|---|
| 첫회기 축어록(2장, 0711tagged) | 45세 어머니, 자녀 등교거부발 우울·분노·신체화, BPS 통합 + 사례개념화 워크시트 | 상호작용 분석 98건 | **P8 미영** (부모 측 페르소나) |
| 게슈탈트 3회기(3장, TAG) | 40대 한부모, 아들 학업갈등 분노/죄책감, 침묵 70회(2~58초)·울음·웃음 정량 | 주석 188건 | **P10 선혜** |
| 자해 DBT(42, tagged) | 성인 직장인 반복 자해, DBT 강의+역할극(타당화 6수준·서약·체인분석·AVIS-R) | 강의 40행+주석 11건 | **P9 유진** (게이티드) |
| 우울 CBT(영상 40, 0712tagged) | 40대 남성 우울·부부갈등, Beck 모델 다회기 발췌 | 상담자 태그 261쌍(최다) | **P11 정호** |
## 2. 이번 세션에서 실행 완료
1. **KB 등록 (P4 업로드 경로 실사용)** — 실명 사전 치환본(은진/이현정/Ashley/Shelly/Carmen Wiebe/Edward/Sharon → 역할명) 4종을
`POST /personas/sources/upload`로 등록. 서버 마스킹(NAME/ORG) 추가 적용, 총 **101 청크 색인+임베딩**.
source_ids: `persona_authoring_9c383fdb7f254d61`(ch2) · `persona_authoring_fa77f213b3664422`(ch3) ·
`persona_authoring_77c1cf9a505b4d77`(dbt) · `persona_authoring_25feeb93ab384e18`(cbt). 원본 바이트 미저장(hash-only 증거).
2. **taxonomy v1.1.0** (`apps/api/app/taxonomy.py`, append-only) — 연구팀 태그 정합을 위해
Technique 9종(명료화·수치화·체인분석·인지재구성·대안행동·전념전략·지금-여기·재진술·기술코칭),
ClientState 9종(표면순응·외재화·지시요청·겉보기유능함·적극적수동성·자해보고·신체증상·감정가림·초점이탈/융합) 추가.
evaluator fast-loop 후보는 `TECHNIQUE_KO` 전체를 쓰므로 자동 반영. 조작적 정의는 `docs/taxonomy.md` 갱신.
3. **가드레일 자해 수단 확장** (`services/guardrail.py`) — DBT 자해 페르소나 대비 `_MEANS_TERMS`
커터/면도날/손목 긋/깊게 긋기/라이터 지지기 추가. "선을 긋다"(경계 설정) 오차단 방지 테스트 포함.
4. **라이브 코칭 소스팩 3종 신설** (`data/kb/live_coaching_sources/`) — DBT 위기·전념 / 게슈탈트 회기 무브 /
부모 첫회기 무브. 요약 재구성(원문 미적재), 기존 0615 스키마 준수.
5. **신규 페르소나 초안 4종 생성** — 등록한 RAG 근거로 `POST /personas/drafts/generate`**draft 상태 저장**.
승인·활성화는 연구팀/교수 검수 몫(월권 방지). 특히 **P9(자해·DBT)는 안전장치 실측·교수 검수 전 승인 금지**.
## 3. 후속 계획 (임상팀·소유자 확인 필요 — 착수 전 게이트)
| 항목 | 내용 | 게이트 |
|---|---|---|
| 평가 golden set 확장 | P8~P11.jsonl — 태그를 신규 taxonomy 라벨로 매핑, 발화는 합성 재작성(출판물 원문 금지). cbt 261쌍이 최대 소스 | 임상팀 라벨 검수 |
| 다회기 스키마 격상 | `Annotation`에 session_no/segment 추가, nonverbal을 {type,raw,duration_s} 구조화(ch3 침묵 분포 정량 활용) | 골든셋 재현성 검토 |
| 이론모드 확장 | `persona.py THEORY_MODE_GUIDANCE`에 gestalt/dbt 모드 신설 (현재 humanistic/cbt/integrative) | 소유자 결정 |
| P1~P7 보강 | P1 침묵 연출·위험사정 골든, P2 신체화 언어, P3 triggers(한부모 낙인), P4 수치화 골든, P6 표면순응 단서, P7 비자발 오프닝, P4~P7 triggers 일괄 | 임상팀 콘텐츠 |
| 사례개념화 루브릭 | ch2 워크시트(BPS 강점·취약, 보호/방해요인 매트릭스)를 `data/rubrics/` draft로 | 임상팀 승인 후 활성 |
| evaluator 이론모드 가중 | 라벨 수 증가에 따른 fast-loop 정밀도 방어(dbt→전념/체인분석 우선 등) | 정확도 벤치 후 |
| 부모-자녀 교차 훈련 세트 | P8(부모)↔P4~P7(자녀) 같은 사건 양측 상담 구성 | 연구팀 교육설계 |
## 4. 저작권·PII 원칙 (재확인)
- ch3(출판 도서)·dbt/cbt(교육 영상 번역물)는 **패턴 추출·합성 재작성만** 허용, 원문 대량 재현 금지.
- 재사용 전 치환 실명 목록: 은진·이현정(ch2) / Ashley·Shelly·Shelley·Carmen Wiebe·Sharon(dbt) / Edward Crane·Sharon(cbt).
Marsha Linehan 등 이론 창시자 학술 인용은 유지 가능.
- 업로드 파이프라인이 서버측 마스킹을 추가 적용하지만, 사전 치환을 표준 절차로 유지한다.