# RFC: `@things-factory/kpi` 프로덕션 완성도 향상

- **상태**: Draft
- **대상 모듈**: `@things-factory/kpi` (v10.0.0-zeta.10)
- **목적**: 이 프레임워크 모듈 자체의 완성도·재사용성 향상. (특정 앱 — 예: `@dssp/dkpi` — 을 위한 것이 아님)
- **작성 근거**: 모듈 소스 코드 감사(2026-07-14). 인용된 `file:line`은 감사 시점 기준.

---

## 1. 배경 & 문제의식

`@things-factory/kpi`는 **계층형 KPI + metric 집계 + 통계**를 갖춘 범용 KPI 프레임워크다. 골격(엔티티, AST 계산기, 통계 엔진)은 견고하다. 그러나 실전 적용(예: DSSP KPI POC)에서 **앱이 프레임워크 관심사를 대신 떠안는** 패턴이 반복 관찰됐다:

- metric ↔ 소스 바인딩(`collectType/dataSet/fieldName`)이 있으나 **쓰기 불편해 앱이 하드코딩 매핑으로 우회**.
- 값 입력 UI가 **metric 이름 추측·하드코딩 위젯** 기반 → admin이 metric을 추가/변경하면 UI가 자동 대응 못 함.
- `(domain, metric, valueDate, org)` upsert·`valueDate` 규칙이 **앱 코드로 샘**.
- "데이터 없음"과 "계산 실패"가 구분되지 않아 앱에서 시나리오가 중단(HALT)되는 사고.

프레임워크의 존재 이유는 **"코드 없이 런타임 설정으로 KPI를 구성"** 하는 것이다. 위 gap들은 그 전제를 프로덕션 규모에서 무너뜨린다. 본 RFC는 이를 메우기 위한 우선순위·설계를 제안한다.

## 2. 목표 / 비목표

**목표**
- 선언(정의)만으로 **수집·입력 UI·검증·계산**이 자동으로 따라오는 구조.
- 계산의 **정확성·안전성·테스트가능성** 확보.
- metric/kpi 정의 **진화(버전)** 를 데이터 정합성 손상 없이 수용.

**비목표**
- 건설 등 특정 도메인 기능 추가(범용성 유지).
- 기존 데이터 파괴적 마이그레이션(모두 하위호환 경로 제공).
- 대시보드/시각화 렌더링 자체(모듈은 정의·저장, 표현은 소비자 몫 — 단 vizMeta 스펙은 문서화).

## 3. 현재 상태 요약 (감사 기반)

| 영역 | 상태 | 완성도 | 핵심 gap |
|---|---|---|---|
| 엔티티 모델 | 있음 | 70% | metric 버전 추적 부재, 렌더힌트 스키마 부재, scope 필드 중복(`group` vs `kpiOrgScope`) |
| 계산 엔진 | 있음(AST 파서/평가기 분리) | 60% | **집계 경로가 `Function()` eval 사용**(안전한 AST 평가기 미사용), 논리연산 부재, 전량 재계산, 결측/실패 미구분 |
| Score 계산 | 있음 | 75% | scoreFormula 검증 부재, formula 변경 후 자동 재계산 트리거 없음, `grades`(deprecated) 혼재 |
| 수집 AUTO/MANUAL | 있음 | 70~80% | EXTERNAL 미구현, metric `schedule` 자동 호출 미연결, collectType별 UI 분기 없음 |
| 통계 | 있음 | 85% | N+1 쿼리 |
| Alert | GraphQL 타입만 | 10% | rule/trigger 엔진 전무 (`kpi-alert-type.ts`, DB 엔티티 아님) |
| 클라이언트 UI | 부분 | 40% | **metadata 기반 제네릭 렌더러 부재**, metric 정의 에디터 부재, 동적 metric 코드수정 없이 렌더 불가 |
| 버전관리 | 부분 | 50% | Kpi는 `@VersionColumn`+`KpiHistory` 있음, **KpiMetricValue는 metric 버전 미스탬프** |
| 테스트 | 없음 | 0% | calculator/formula/aggregate 전부 무테스트 |
| 보안 | 부분 | 50% | **formula `Function()` eval → injection 위험** (`aggregate-kpi.ts:53`) |

강점(유지): AST 파서/평가기 분리(`server/calculator/parser.ts`, `evaluator.ts`, `functions.ts`), 통계 엔진(`kpi-statistic-calculation.service.ts`), 계층형 KPI + KpiHistory 스냅샷, 다차원 scope(KpiScope level1~5).

## 4. 설계 원칙

1. **선언적, 비-마법적(declarative, not magical)**: 동작은 정의(메타데이터)에서 결정한다. 이름 추측·값 추론 금지.
2. **프레임워크가 관심사를 소유**: valueDate/upsert/집계 규칙은 모듈 API 뒤로 숨긴다. 앱은 "무엇을/언제"만 말한다.
3. **80/20 + 탈출구**: 제네릭이 80%를 자동 처리, 예외는 커스텀 레지스트리로.
4. **결측 ≠ 실패**: 데이터 없음은 정상 상태(1급), 실패는 별도.
5. **진화 가능**: 정의 변경이 과거 데이터를 깨지 않도록 버전 스탬프.

---

## 5. 제안 (우선순위별)

### P0 — 정확성·안전성·신뢰 (선행 필수)

#### P0-1. 안전한 단일 계산 경로로 통일 (eval 제거)
- **문제**: 안전한 AST 파서/평가기(`server/calculator/*`)가 있음에도, 집계는 `new Function(...)`로 formula를 eval한다 → **코드 인젝션 위험 + 검증 불가 + silent fail**. 근거: `server/service/kpi/aggregate-kpi.ts:53`.
- **제안**: 모든 formula/scoreFormula 평가를 **AST 평가기 하나로 통일**하고 `Function()` 경로 삭제. 평가기에 논리/비교 연산(`&&`, `||`, `>`, `<`, `>=`, `<=`, `==`), 삼항 대체(`if()`는 유지) 추가.
- **효과**: 보안 + 정적 검증 가능 + 결정적 테스트 가능.

#### P0-2. Formula 검증기 완성
- **문제**: `validateFormula()`가 metric 존재·순환참조는 보나 **문법(괄호/토큰)·함수 존재 검증은 주석만**. 근거: `server/service/kpi/kpi-formula.service.ts:33-65`.
- **제안**: 파서를 재사용해 (a) 구문 파싱 성공, (b) 사용 함수가 `functions.ts` 화이트리스트에 존재, (c) 미정의 변수 없음 을 **정의 저장 시점에 검증·거부**. 저장 mutation에서 강제.

#### P0-3. 결측 vs 실패 시맨틱 + 상태/프로버넌스
- **문제**: metric 값 부재는 저장 안 함, 계산 실패도 `value=null` 후 skip → **구분 불가·추적 불가**. 근거: `aggregate-kpi.ts:60`, `kpi-value-score.service.ts`(null 반환).
- **제안**: `KpiValue`/`KpiMetricValue`에 **상태 개념** 도입 — `OK | NO_DATA | ERROR`(+ `meta.error`). 계산기는 예외를 삼키지 말고 `ERROR` 상태 + 사유 기록. 조회/집계는 `NO_DATA`를 정상 전파(null propagation 규칙 명문화).
- **효과**: 앱이 "없음"에 중단하지 않고 진행(HALT 방지), 운영 모니터링 가능.

#### P0-4. Metric 버전 스탬프
- **문제**: `Kpi`는 `@VersionColumn`+`KpiHistory` 스냅샷이 있으나(`kpi.ts`, `kpi-history.ts`), **`KpiMetricValue`는 metric 버전을 안 남김**(`kpi-metric-value.ts:1-93`). metric 정의(periodType/unit/dataSet) 변경 후 과거 값 해석 불가.
- **제안**: `KpiMetric`에 version, `KpiMetricValue`에 기록 시점 `metricVersion` 스탬프. 정의 변경 시 `KpiMetricHistory` 스냅샷(기존 KpiHistory 패턴 재사용).

#### P0-5. 테스트 스위트 구축
- **문제**: `*.test.ts`/`*.spec.ts` 전무, package.json test script 없음.
- **제안**: 최우선으로 **순수 로직**(parser, evaluator, functions, formula.service, score.service, aggregate)에 단위테스트. CI 게이트. (P0-1~3의 회귀 안전망)

### P1 — 선언적 자동화 (프레임워크의 본질)

#### P1-1. 선언적 정의 스키마 + 렌더 힌트 (핵심 레버)
- **문제**: 정의에 "입력/표현을 어떻게 할지" 메타가 없어, UI가 **이름 추측**(예: scoreType 추론 `kpi-list-page.ts:54-60`)과 하드코딩에 의존. `unit`도 자유 문자열.
- **제안**: metric/kpi 정의를 확장:
  ```ts
  // KpiMetric 확장
  kind: 'attribute' | 'performance'          // 고정속성 vs 시계열성과
  source: { type: 'entityField' | 'dataset' | 'external' | 'manual', ... }
  input: { widget: 'number'|'rating'|'select'|'date'|'text',
           min?, max?, step?, options?, required? }   // 렌더/검증 힌트
  aggregation?: 'sum'|'avg'|'last'|'min'|'max'
  ```
  - `kind`는 **reference(고정 속성) vs performance(성과)** 를 모델에 각인 → 소비 앱의 "속성↔KPI 양방향 sync" 같은 안티패턴 원천 차단.
  - `input`은 위젯·검증을 **정의가 소유**. (별점 0.5→1점 같은 요구가 코드가 아니라 `input.step`로 해결)

#### P1-2. Metadata-driven 제네릭 렌더러 + 커스텀 레지스트리
- **문제**: 값 입력 UI가 하드코딩, 동적 metric은 코드 수정 없이 못 뜬다(`kpi-metric-value-manual-entry-form.ts`, `kpi-value-editor-page.ts`).
- **제안**: `input` 힌트를 읽어 위젯을 렌더하는 **제네릭 렌더러 1개** + 예외용 **custom renderer registry**(80/20). periodType(단일/월별그리드/기간) 인지 레이아웃 포함.

#### P1-3. 값 인입 API (upsert/valueDate 캡슐화)
- **문제**: `(domain,metric,valueDate,org)` upsert·MONTH/ALLTIME valueDate 규칙이 앱으로 샘.
- **제안**: `kpi.record({ metric, value, org, period, at? })` 단일 진입점이 valueDate 정규화·upsert·버전스탬프·상태를 내부 처리. period/bucket을 문자열 관례 대신 타입으로.

#### P1-4. 수집(collector) 추상화
- **문제**: `collectType` enum은 있으나 EXTERNAL 미구현, metric `schedule` 자동 호출 미연결(스케줄은 KPI에만 `registerSchedule` — `kpi-mutation.ts:76-110`), 수집이 앱/시나리오에 의존.
- **제안**: metric별 **Collector 인터페이스**(`collect(): {found, value, provenance}`)와 스케줄러 연동을 모듈이 제공. AUTO(dataset)·MANUAL은 기본 제공, EXTERNAL/IMPORT는 소비자가 collector 등록. "found=false"는 정상.

### P2 — 정리·운영

- **P2-1. Deprecated 필드 정리**: `grades`→`scoreFormula`(`kpi-value-score.service.ts:51`), `KpiValue.group`(string)→`kpiOrgScope`(ref). 마이그레이션 + 이중지원 종료 계획.
- **P2-2. Scope 필드 통합**: `org`/`group`/`kpiOrgScope`/`project`를 **하나의 scope(dimension) 모델**로 수렴, 나머지 deprecate.
- **P2-3. 통계 N+1 제거**: `kpi-statistic-calculation.service.ts:154-210`, `aggregate-kpi-metric.ts:66` batch/조인.
- **P2-4. Alert rule 엔진**: 현 `kpi-alert-type.ts`는 타입만 → 조건·트리거·저장 엔티티 설계.
- **P2-5. 권한 세분화**: 현 `kpi:mutation|query` 2단계 → metric정의/값입력/조회 분리.
- **P2-6. 문서화**: formula/scoreFormula 문법·함수 목록, `vizMeta` 스펙(vizType별 필드), KpiScope(scope01~05) taxonomy, cron 가이드.

---

## 6. 마이그레이션 & 하위호환

- 신규 필드(kind/source/input, metricVersion, status)는 **nullable + 기본값**으로 추가 → 기존 데이터 무손상.
- 기존 정의에 `kind`/`input` 미지정 시: 렌더러는 **안전한 폴백**(number 위젯) + 콘솔 경고. (점진 채택)
- eval→AST 전환은 기존 formula 문법을 **파서가 수용하는지 회귀테스트**(P0-5) 후 전환. 미수용 문법은 마이그레이션 스크립트로 정규화.
- `grades`/`group`은 **읽기 이중지원 유지**하며 한 마이너 버전 뒤 제거 공지.

## 7. 리스크

- **eval→AST 전환**이 기존 프로덕션 formula를 깨뜨릴 수 있음 → 회귀 코퍼스 + 파서 수용성 검증 선행(P0-5, P0-1 순서 고정).
- 선언 스키마 확장은 **소비 앱들이 서서히 채택**해야 효과 → 폴백을 보장해 강제 마이그레이션 회피.
- 범용성 훼손 위험 → 모든 신규는 **도메인 중립 메타**만(건설 특화 금지).

## 8. 단계 로드맵

- **Phase 1 (신뢰 기반)**: P0-5 테스트 → P0-1 eval 제거/통일 → P0-2 검증기 → P0-3 상태 시맨틱 → P0-4 버전 스탬프.
- **Phase 2 (선언 자동화)**: P1-1 스키마 → P1-3 record API → P1-2 제네릭 렌더러 → P1-4 collector.
- **Phase 3 (정리·운영)**: P2-*.

각 Phase는 독립 배포 가능하며 하위호환을 유지한다.

---

## 부록 A. 핵심 근거 (file:line)

- 계산 eval: `server/service/kpi/aggregate-kpi.ts:53`, null skip `:60`
- AST 계산기(재사용 대상): `server/calculator/parser.ts`, `evaluator.ts`, `functions.ts:4-67`
- formula 검증 미흡: `server/service/kpi/kpi-formula.service.ts:33-65`
- score/ grades deprecated: `server/service/kpi-value/kpi-value-score.service.ts:51`
- metric 정의(버전/렌더힌트 부재): `server/service/kpi-metric/kpi-metric.ts:1-138`
- metric value(버전 미스탬프): `server/service/kpi-metric-value/kpi-metric-value.ts:1-93`
- scope 중복: `server/service/kpi-value/kpi-value.ts` (`group` vs `kpiOrgScope`)
- 하드코딩 UI: `client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.ts`, `client/pages/kpi-value/kpi-value-editor-page.ts`
- 이름 추측: `client/pages/kpi/kpi-list-page.ts:54-60`
- alert 타입만: `server/service/kpi-alert/kpi-alert-type.ts:1-20`
- 통계 N+1: `server/service/kpi-statistic/kpi-statistic-calculation.service.ts:154-210`
- 스케줄(KPI만): `server/service/kpi/kpi-mutation.ts:76-110`
