회기 무발화 0턴 분리, 자기예측 락 불변식 및 TDD 회귀 검증 완료
Some checks failed
API contract / OpenAPI type drift (push) Failing after 3m27s

This commit is contained in:
Yun Chan 2026-09-08 23:28:06 +09:00
parent a479db7a5a
commit a0311c5957
100 changed files with 4884 additions and 11210 deletions

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 예산을 함께 갱신한다.
- 라우트 컨테이너 추출은 독립 입력/출력, 독립 상태, 독립 테스트가 생긴 뒤 한다. 파일 길이만으로 분리하지 않는다.