Chapter 3에서는 불량 데이터를 집계해 추이와 점유율을 보여주는 대시보드를 만들었습니다. 이번 Chapter 4에서는 한 걸음 더 들어갑니다. 집계된 건수가 아니라, 검사에서 나온 측정값 그 자체를 통계적으로 분석하는 도구를 만듭니다. 품질 엔지니어라면 한 번쯤 Minitab으로 돌려봤을 Cpk, 관리도, ANOVA, 상관분석을 한 화면에 모은 통계분석 툴입니다.
한 가지 달라진 점이 있습니다. Chapter 3의 대시보드는 설명을 위해 CSV 한 장을 직접 읽었지만, 이번 도구는 실제 운영에 가깝게 PostgreSQL DB에서 공정별 측정 데이터를 조회합니다. 9개 공정(P01~P09) 각각의 검사 테이블을 두고, 통계 계산(Cpk, ANOVA, 회귀)은 모두 백엔드에서 NumPy·SciPy로 처리한 뒤 JSON으로 내려줍니다. 프론트(Next.js + Recharts)는 받은 결과를 그리는 데만 집중합니다.
화면은 상단 토글 탭 네 개로 전환합니다.
- Cpk Analysis — Cp / Cpk / Ppk 산출과 X-bar · R 관리도.
- ANOVA — 제품군·라인 간 평균 차이의 통계적 유의성 검정.
- Scatter · Anomaly — 측정값 시계열의 2σ / 3σ 이상치 탐지.
- Scatter · Correlation — 두 측정 항목 간 상관계수 · 선형 회귀.
만드는 방식은 지난번과 같습니다
Chapter 3에서 말씀드린 흐름 그대로입니다. 제가 Claude Code에 건넨 것은 만들고 싶은 목표, 기술 스택, 데이터 설명(공정별 테이블 매핑과 규격 한계), 프론트·백엔드 포트, 원하는 기능, 그리고 화면 느낌을 담은 예시 이미지 몇 장뿐이었습니다. 그러면 아키텍처, 프로젝트 구조, API·컴포넌트 명세, 그리고 관리도 상수나 ANOVA 같은 통계 계산 로직까지 Claude Code가 스스로 검토해 아래 PRD로 정리하고 구현했습니다. 통계 공식을 제가 일일이 코드로 옮긴 것이 아니라, "Cpk와 X-bar·R 관리도를 산출해달라"는 요구를 SciPy·NumPy 기반 구현으로 풀어낸 쪽은 에이전트였습니다.
아래 PRD 전문을 그대로 복사해 프로젝트 루트의 PRD.md로 저장하고, Claude Code에게 "이 PRD대로 만들어줘"라고 요청하면 같은 도구를 재현할 수 있습니다. 한 가지만 주의하세요. PRD 안에는 DB 접속 정보가 들어갑니다. 공개용으로 비밀번호와 계정은 가려두었으니, 직접 쓰실 때는 <YOUR_DB_USER>, <YOUR_DB_PASSWORD> 자리에 본인 환경의 값을 채워 넣으면 됩니다.
PRD 전문 펼치기 / 접기 (클릭)
# PRD — Statistical Analysis Dashboard
**작성일**: 2026-06-18
**버전**: 1.0
---
## 1. 개요
배터리 제조 공정의 검사 데이터를 기반으로 통계적 품질 분석을 수행하는 독립형 웹 대시보드.
사이드바 없음. 상단 4개 토글 탭으로 분석 화면을 전환한다.
PostgreSQL DB에서 공정별 측정 데이터를 조회하며, 백엔드에서 통계 계산(Cpk, ANOVA, 상관분석)을 수행한 후 JSON으로 반환한다.
---
## 2. 목표
- 공정별 Cp / Cpk / Ppk 산출 및 X-bar · R 관리도 시각화
- One-way ANOVA로 제품군·라인 간 평균 차이의 통계적 유의성 검정
- 측정값 시계열에서 2σ / 3σ 기준 이상치 자동 탐지 및 강조
- 두 측정 항목 간 Pearson 상관계수 · 선형 회귀 · p-value 산출
- 분석 조건(공정·항목·기간 필터)은 탭 전환 후에도 localStorage로 유지
---
## 3. 기술 스택
| 구분 | 기술 | 버전 |
|---|---|---|
| Frontend | Next.js | 16.2.7 |
| Frontend | React / React-DOM | 19.2.4 |
| Frontend | TypeScript | ^5 |
| Frontend | Tailwind CSS | ^4 |
| Frontend | Recharts | ^3.8.1 |
| Backend | Python | 3.11+ |
| Backend | FastAPI | latest |
| Backend | Uvicorn | latest |
| Backend | psycopg2-binary | latest |
| Backend | NumPy | latest |
| Backend | SciPy | latest |
| DBMS | PostgreSQL | 15+ |
### 포트
| 서비스 | URL |
|---|---|
| Frontend | `http://localhost:3003` |
| Backend | `http://localhost:8003` |
---
## 4. 데이터 소스
**DB**: PostgreSQL (`localhost:5432`, `dbname=portfolio`)
### 공정 테이블 매핑
| 공정 코드 | 테이블명 | 주요 측정 항목 |
|---|---|---|
| P01 | `insp_p01_cell_incoming` | ocv_meas, ocv_ship, delta_ocv, acir |
| P02 | `insp_p02_tab_cutting` | tab_length, bending_angle |
| P03 | `insp_p03_tape` | x1, x2, y1, y2 |
| P04 | `insp_p04_stacking` | protrusion |
| P05 | `insp_p05_busbar_bending` | bend_length |
| P06 | `insp_p06_bolt` | rundown_angle, final_torque |
| P07 | `insp_p07_dimension` | width, length, height |
| P08 | `insp_p08_charge_discharge` | dcir, charge_current, discharge_current |
| P09 | `insp_p09_eol` | insulation_resist, total_voltage, dark_current |
### 공통 조인 구조
```sql
FROM {process_table} t
JOIN inspection_header ih ON ih.id = t.header_id
JOIN dim_product dp ON dp.id = ih.product_id
JOIN dim_line dl ON dl.id = ih.line_id
```
### 규격 한계 (SPEC_LIMITS)
백엔드 `routers/statistics.py`에 딕셔너리로 관리. SQL Injection 방지를 위해 공정·항목 코드는 화이트리스트(PROCESS_ITEM_MAP)로만 허용.
| 공정 | 항목 | LSL | USL |
|---|---|---|---|
| P01 | ocv_meas | 3.550 | 3.750 |
| P01 | delta_ocv | -0.065 | 0.0 |
| P01 | acir | — | 2.0 |
| P02 | tab_length | 8.20 | 8.80 |
| P02 | bending_angle | 88.0 | 92.0 |
| P03 | x1/x2/y1/y2 | -0.50 | 0.50 |
| P04 | protrusion | 1.700 | 2.300 |
| P05 | bend_length | 4.500 | 5.500 |
| P06 | rundown_angle | 330.0 | 390.0 |
| P06 | final_torque | 3.200 | 3.800 |
| P07 | width | 149.50 | 150.50 |
| P07 | length | 299.50 | 300.50 |
| P07 | height | 109.50 | 110.50 |
| P08 | dcir | — | 5.0 |
| P09 | insulation_resist | 100.0 | — |
| P09 | total_voltage | 35.000 | 37.500 |
| P09 | dark_current | — | 50.0 |
---
## 5. 프로젝트 구조
```
D:\Dev\03-2.statistical_analysis\
├── docs\
│ ├── PRD.md
│ ├── screen_shot\ # Playwright 자동 스크린샷
│ └── UI\ # 화면 디자인 참조 이미지
├── src\
│ ├── backend\
│ │ ├── main.py # FastAPI 앱 진입점 (포트 8003)
│ │ ├── database.py # ThreadedConnectionPool, query()
│ │ ├── requirements.txt
│ │ ├── routers\
│ │ │ ├── __init__.py
│ │ │ ├── master.py # GET /api/master/products, /lines
│ │ │ └── statistics.py # GET /api/statistics/cpk, /anova, /scatter
│ │ └── services\
│ │ ├── __init__.py
│ │ ├── cpk_service.py # Cp/Cpk/Ppk, X-bar/R 관리도 계산
│ │ └── anova_service.py # One-way ANOVA, 정규분포 곡선 생성
│ └── frontend\
│ ├── app\
│ │ ├── globals.css
│ │ ├── layout.tsx # 사이드바 없음, 100vh flex column
│ │ └── page.tsx # 4탭 헤더 + 공유 필터 상태
│ ├── components\
│ │ ├── FilterBar.tsx # 공통 필터 바 (extra slot 제공)
│ │ ├── Card.tsx # Card + KpiCard
│ │ ├── CpkView.tsx # Cpk 분석 화면
│ │ ├── AnovaView.tsx # ANOVA 분석 화면
│ │ ├── AnomalyView.tsx # 이상치 탐지 화면
│ │ └── CorrelationView.tsx # 상관분석 화면
│ ├── lib\
│ │ ├── api.ts # fetch 래퍼 (BASE: localhost:8003/api)
│ │ └── hooks.ts # useStoredState (SSR 안전 localStorage hook)
│ ├── package.json # name: statistical-analysis, port 3003
│ ├── tsconfig.json
│ ├── next.config.ts
│ └── postcss.config.mjs
└── start.bat # 백엔드/프론트엔드 동시 실행
```
---
## 6. 아키텍처
```
[ Browser :3003 ]
|
| fetch (no-store)
|
[ Next.js Frontend ] ──── REST API ──── [ FastAPI Backend :8003 ]
|
psycopg2 ThreadedConnectionPool
PostgreSQL :5432 / portfolio
```
- DB 연결은 `ThreadedConnectionPool(2, 10)`으로 풀링
- 통계 계산(Cpk, ANOVA, 회귀)은 NumPy / SciPy로 백엔드에서 전담
- CORS: `allow_origins=["http://localhost:3003"]`
---
## 7. 화면 구성
### 7.1 공통 레이아웃
```
┌────────────────────────────────────────────────────────────────────┐
│ S Statistics │ [ Cpk Analysis ] [ ANOVA ] [ Scatter·Anomaly ] │
│ ANALYSIS │ [ Scatter·Correlation ] 우측 탭 토글 │
├────────────────────────────────────────────────────────────────────┤
│ 서브헤더: 현재 탭 분석 설명 (밝은 배경 + 회색 텍스트) │
├────────────────────────────────────────────────────────────────────┤
│ FILTER [All Products ▼] [All Lines ▼] [날짜~날짜] │
│ ───────────────────────────── PROCESS SELECT [▼] [분석 실행]│
├────────────────────────────────────────────────────────────────────┤
│ KPI Cards (탭별 상이) │
├────────────────────────────────────────────────────────────────────┤
│ 차트 영역 (탭별 상이) │
└────────────────────────────────────────────────────────────────────┘
```
**공통 규칙**
- 사이드바 없음 — 전체 너비 사용
- 4탭 토글: Header 우측, 활성 탭은 오렌지(`#fb923c`) 배경 + 흰 텍스트
- URL 이동 없이 `activeTab` state로 조건부 렌더링
- 공유 필터(product, line, date_from, date_to): `page.tsx`에서 `useStoredState("stat_filters")`로 관리 → 모든 탭에 props로 전달
- 탭별 개별 선택값(공정, 항목 등): 각 View 컴포넌트에서 독립적으로 `useStoredState`로 관리
- **수동 실행**: 모든 탭에 `분석 실행` 버튼(오렌지) — 자동 실행 없음, 버튼 클릭 시에만 API 호출
- 배경색: `#f8fafc` (`--bg-primary`)
---
### 7.2 Tab 1 — Cpk Analysis
**서브헤더**: `Process capability: Cp / Cpk / Ppk + X-bar R charts`
#### FilterBar 추가 컨트롤 (우측 정렬)
| 컨트롤 | 설명 |
|---|---|
| PROCESS SELECT 드롭다운 | P01 ~ P10 공정 선택 |
| 항목 드롭다운 | 선택 공정의 측정 항목 |
| 서브그룹 기준 토글 | 고정(5개) / 일별 / 주차별 / 월별 |
| 분석 실행 버튼 | 클릭 시 `/api/statistics/cpk` 호출 |
#### KPI Cards (6개)
| 라벨 | 값 | 색상 |
|---|---|---|
| N | 전체 데이터 수 | 기본 |
| Mean | 평균값 (소수점 4자리) | 기본 |
| σ Within | R̄/d₂ 기반 내부 표준편차 | 기본 |
| Cp | 양측 공정 능력 | Cpk 색상 체계 |
| Cpk | 단측 공정 능력 (min) | ≥1.67 녹색, ≥1.33 파란, ≥1.0 노란, 미만 빨강 |
| Ppk | 전체 표준편차 기반 성능 지수 | Cpk 색상 체계 |
#### 차트 구성 (2×2 그리드)
**① Histogram + Normal Curve** (좌상)
- `ComposedChart`: Bar(측정 빈도) + Line(정규 분포 곡선)
- X축: numeric, 자동 범위 → LSL/USL 포함하도록 확장
- 카드 헤더 우측: 회색 텍스트로 `USL = {값}` / `LSL = {값}` 표시
- LSL · USL: 붉은 점선 ReferenceLine (`stroke="#dc2626"`, `strokeDasharray="4 4"`)
**② Capability Summary** (우상)
- 텍스트 테이블: n, Mean, Within-σ, Overall-σ, R̄, Cp, Cpk, Ppk
**③ X-bar Chart** (좌하)
- `LineChart`: 서브그룹 평균 추이
- CL (녹색 점선) · UCL · LCL (붉은 점선)
- Y축 domain: `[min(data, LCL) - margin, max(data, UCL) + margin]`
- X축 레이블: 서브그룹 번호 or 기간 레이블 (일별/주차별/월별)
**④ R Chart** (우하)
- `LineChart`: 서브그룹 범위(Range) 추이
- CL · UCL (붉은 점선), LCL은 > 0 일 때만 표시
- Y축 domain: `[0, max(data, UCL) + margin]`
---
### 7.3 Tab 2 — ANOVA
**서브헤더**: `One-way ANOVA with bell curve overlay`
#### FilterBar 추가 컨트롤
| 컨트롤 | 설명 |
|---|---|
| PROCESS SELECT 드롭다운 | P01, P02, P04~P09 |
| 항목 드롭다운 | 선택 공정의 측정 항목 |
| 그룹 기준 드롭다운 | By Product / By Line |
| 분석 실행 버튼 | 클릭 시 `/api/statistics/anova` 호출 |
#### KPI Cards (5개)
| 라벨 | 값 | 색상 |
|---|---|---|
| F-Statistic | F 검정 통계량 | 유의 시 빨강 |
| p-value | ANOVA p-value (소수점 6자리) | 유의 시 빨강, 비유의 시 녹색 |
| Significance | YES ★ / No | 유의 시 빨강, 비유의 시 녹색 |
| df Between | 그룹 간 자유도 | 기본 |
| df Within | 그룹 내 자유도 | 기본 |
#### 차트 구성
**① Normal Distribution Overlay**
- `AreaChart`: 그룹별 정규분포 곡선 (fillOpacity 0.3)
- 헤더 우측: 그룹별 pill 버튼 — 클릭으로 개별 그룹 표시/숨김
- 색상: `GROUP_COLORS` 10색 팔레트 순환
**② Group Statistics Table**
- 컬럼: Group · N · Mean · Std Dev · Min · Q1 · Median · Q3 · Max
- 그룹 색상 사각형 마커 표시
---
### 7.4 Tab 3 — Scatter · Anomaly
**서브헤더**: `Measurement time-series ordered by inspection date · 2σ / 3σ threshold bands`
#### FilterBar 추가 컨트롤
| 컨트롤 | 설명 |
|---|---|
| PROCESS SELECT 드롭다운 | P01 ~ P09 |
| Y항목 드롭다운 | 선택 공정의 측정 항목 |
| 분석 실행 버튼 | 클릭 시 `/api/statistics/scatter` 호출 |
#### KPI Cards (4개)
| 라벨 | 값 | 색상 |
|---|---|---|
| Total Points | 전체 데이터 포인트 수 | 기본 |
| Mean | 평균 (지수 표기 자동 전환) | 기본 |
| Std Dev (σ) | 표본 표준편차 | 기본 |
| Anomaly (> 3σ) | 이상치 수 + % | 이상치 있으면 빨강 |
#### 차트 구성
**① 시계열 산점도**
- `ScatterChart`: X = 검사 순서(chronological index), Y = 측정값
- 3가지 색상 구분:
- Normal (|y - μ| ≤ 2σ): 파란색 (`#2563eb`), 반투명
- Warning (2σ < |y - μ| ≤ 3σ): 오렌지 (`#f97316`)
- Anomaly (|y - μ| > 3σ): 빨강 (`#dc2626`), 불투명
- Reference Lines: μ(회색), ±2σ(오렌지), ±3σ(빨강) 점선
---
### 7.5 Tab 4 — Scatter · Correlation
**서브헤더**: `Statistical correlation between two measurement items · linear regression & significance test`
#### FilterBar 추가 컨트롤
| 컨트롤 | 설명 |
|---|---|
| PROCESS SELECT 드롭다운 | P01 ~ P09 (2개 이상 항목 있는 공정) |
| X항목 드롭다운 | X축 측정 항목 |
| Y항목 드롭다운 | Y축 측정 항목 (X와 동일 선택 불가) |
| 분석 실행 버튼 | 클릭 시 `/api/statistics/scatter` 호출 |
#### KPI Cards (5개)
| 라벨 | 값 | 색상 |
|---|---|---|
| N (Data Points) | 산점도 포인트 수 | 기본 |
| Pearson r | 상관계수 (소수점 4자리) | |r|>0.7 빨강, >0.4 노란, 이하 녹색 |
| R² (Explained) | 결정계수 | 동일 색상 체계 |
| p-value | 유의확률 (★★★ < 0.001, ★★ < 0.01, ★ < 0.05) | 유의 시 빨강/노란 |
| Correlation | Strong/Moderate/Weak/No Positive/Negative | 동일 색상 체계 |
#### 차트 구성
**① 산점도 + 회귀선**
- `ComposedChart`: Scatter(데이터 포인트) + Line(회귀선)
- 회귀선: 붉은 점선 (`stroke="#ef4444"`, `strokeDasharray="5 3"`)
- 차트 헤더 서브텍스트: `y = {slope}x ± {intercept} · r = {r}`
---
## 8. API 명세
### Base URL
`http://localhost:8003/api`
---
### `GET /api/master/products`
**응답**
```json
[
{ "value": "PRD-001", "label": "PRD-001" }
]
```
---
### `GET /api/master/lines`
**응답**
```json
[
{ "value": "L01", "label": "L01" }
]
```
---
### `GET /api/statistics/cpk`
| Query Param | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `process_code` | string | 필수 | P01 ~ P10 |
| `item_code` | string | 필수 | 화이트리스트 항목명 |
| `product_code` | string | — | 제품 필터 |
| `line_code` | string | — | 라인 필터 |
| `date_from` | string | — | YYYY-MM-DD |
| `date_to` | string | — | YYYY-MM-DD |
| `subgroup_size` | int | 5 | 고정 서브그룹 크기 |
| `period` | string | `size` | `size` / `day` / `week` / `month` |
**응답**
```json
{
"usl": 150.50,
"lsl": 149.50,
"stats": {
"n": 168, "mean": 150.0047, "std_within": 0.13484,
"std_overall": 0.13195, "cp": 1.234, "cpk": 1.220, "ppk": 1.265, "r_bar": 0.31364
},
"histogram": {
"bins": [149.6, 149.7, ...],
"counts": [3, 8, ...],
"normal_x": [...], "normal_y": [...]
},
"xbar_chart": {
"xbars": [150.02, 149.98, ...],
"labels": ["1", "2", ...],
"cl": 150.0047, "ucl": 150.186, "lcl": 149.824
},
"r_chart": {
"ranges": [0.31, 0.28, ...],
"labels": ["1", "2", ...],
"cl": 0.31364, "ucl": 0.663, "lcl": 0.0
}
}
```
**period별 서브그룹 처리**
- `size`: 연속 N개를 서브그룹으로 분할
- `day` / `week` / `month`: 날짜 기준 그룹핑 후 그룹별 X-bar / Range 계산
---
### `GET /api/statistics/anova`
| Query Param | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `process_code` | string | 필수 | |
| `item_code` | string | 필수 | |
| `group_by` | string | `product` | `product` / `line` |
| `product_code` | string | — | |
| `line_code` | string | — | |
| `date_from` | string | — | |
| `date_to` | string | — | |
**응답**
```json
{
"anova_table": {
"f_stat": 12.34, "p_value": 0.000123,
"significant": true, "df_between": 4, "df_within": 995
},
"curve_x": [3.55, 3.56, ...],
"group_stats": [
{
"label": "PRD-001", "n": 200, "mean": 3.661, "std": 0.021,
"min": 3.590, "max": 3.730, "q1": 3.645, "median": 3.660, "q3": 3.677,
"curve_y": [0.001, 0.003, ...]
}
]
}
```
---
### `GET /api/statistics/scatter`
| Query Param | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `process_code` | string | 필수 | |
| `item_x` | string | 필수 | X축 항목 (Anomaly 탭은 item_x = item_y) |
| `item_y` | string | 필수 | Y축 항목 |
| `product_code` | string | — | |
| `line_code` | string | — | |
| `date_from` | string | — | |
| `date_to` | string | — | |
| `max_points` | int | 2000 | 최대 반환 포인트 수 |
**응답**
```json
{
"points": [
{ "barcode": "BM-...", "product_code": "PRD-001", "line_code": "L01",
"inspected_at": "2026-01-02 19:34", "x": 3.642, "y": 1.60 }
],
"correlation": 0.7234,
"r_squared": 0.5233,
"p_value": 0.000012,
"regression": { "slope": 0.123456, "intercept": -0.045678, "x_min": 3.55, "x_max": 3.75 },
"x_item": "ocv_meas",
"y_item": "acir"
}
```
---
## 9. 컴포넌트 명세
### `app/layout.tsx`
- 사이드바 없음, `<body>` 전체 너비 사용
- `height: 100vh`, `overflow: hidden`, `flex-direction: column`
### `app/page.tsx`
- 상태: `activeTab: "cpk" | "anova" | "anomaly" | "correlation"` — `useStoredState("stat_active_tab")`
- 상태: `filters` — `useStoredState("stat_filters", FILTER_DEFAULT)` (모든 탭 공유)
- Header 렌더: 좌측 브랜드 로고 + 우측 4개 탭 버튼
- `activeTab` 에 따라 해당 View 컴포넌트 조건부 렌더링
### `components/FilterBar.tsx`
Props: `products`, `lines`, `filters`, `onChange`, `extra`
- Product / Line 드롭다운, Date From/To 입력
- `extra` slot: `marginLeft: "auto"` 우측 정렬 — 탭별 공정 선택 + 분석 실행 버튼 삽입
### `components/Card.tsx`
- `Card`: 다크 헤더(`--sidebar-bg`) + 콘텐츠 영역
- `KpiCard`: 라벨(10px uppercase) + 수치(22px bold 900), `color` prop으로 수치 색상 지정
### `components/CpkView.tsx`
- 상태: `proc`, `item`, `period` — 각각 `useStoredState`
- `load`: `useCallback` — 버튼 클릭 시만 호출 (`useEffect` 자동 실행 없음)
- 서브그룹 기준 토글: pill 버튼 그룹 (`고정(5개)` / `일별` / `주차별` / `월별`)
### `components/AnovaView.tsx`
- 상태: `proc`, `item`, `groupBy` — 각각 `useStoredState`
- 그룹 토글: `hidden: Set<string>` — 벨 커브 개별 on/off
### `components/AnomalyView.tsx`
- 상태: `proc`, `itemY` — 각각 `useStoredState`
- 통계 계산(mean, std, 2σ/3σ 분류): 프론트엔드에서 수행 (raw points 수신 후 처리)
### `components/CorrelationView.tsx`
- 상태: `proc`, `itemX`, `itemY` — 각각 `useStoredState`
- `regLine`: 회귀 직선 좌표 81개 포인트 계산 (x_min ~ x_max 균등 분할)
### `lib/api.ts`
- Base URL: `http://localhost:8003/api`
- 노출 메서드: `api.products()`, `api.lines()`, `api.cpk(p)`, `api.anova(p)`, `api.scatter(p)`
### `lib/hooks.ts`
- `useStoredState<T>(key, defaultValue)` — SSR 안전 localStorage 동기화 hook
- 초기 렌더: `defaultValue` 사용 (hydration mismatch 방지)
- mount 후: `localStorage` 값으로 덮어쓰기
---
## 10. 디자인 토큰
### CSS 변수 (`globals.css`)
```css
:root {
--bg-primary: #f8fafc;
--bg-secondary: #ffffff;
--bg-card: #f1f5f9;
--border: #e2e8f0;
--accent: #fb923c;
--accent-blue: #2563eb;
--green: #16a34a;
--red: #dc2626;
--yellow: #d97706;
--text-primary: #0f172a;
--text-secondary: #334155;
--text-muted: #94a3b8;
--sidebar-bg: #1e2530; /* 차트 카드 다크 헤더 */
--sidebar-text: #9ca3af;
}
```
### 차트 색상 팔레트
```ts
// 그룹 구분 (ANOVA, 범례 등 — 최대 10개)
const GROUP_COLORS = [
"#fb923c","#2563eb","#16a34a","#dc2626","#7c3aed",
"#0891b2","#d97706","#be185d","#059669","#9333ea",
];
```
### 스타일 원칙
- **inline style 우선** — Tailwind는 `globals.css` `@import "tailwindcss"` 선언만 사용
- 차트 라이브러리: **Recharts 고정** (Chart.js, D3 사용 안 함)
- 모든 Recharts Line/Scatter: `isAnimationActive={false}` (렌더링 안정성)
- 관리도 한계선: `stroke="#dc2626"`, `strokeDasharray="4 4"`, `strokeWidth={1.5}`
- 중심선(CL): `stroke="#16a34a"`, `strokeDasharray="4 4"`
---
## 11. 백엔드 구현 상세
### `database.py`
```python
from psycopg2.pool import ThreadedConnectionPool
from psycopg2.extras import RealDictCursor
pool = ThreadedConnectionPool(2, 10,
host="localhost", port=5432,
dbname="portfolio", user="<YOUR_DB_USER>", password="<YOUR_DB_PASSWORD>"
)
def query(sql: str, params=None) -> list[dict]:
conn = pool.getconn()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(sql, params)
return [dict(r) for r in cur.fetchall()]
finally:
pool.putconn(conn)
```
### `services/cpk_service.py` — 핵심 계산
```python
# 관리도 상수 (서브그룹 크기 n = 2~10)
_A2 = {2:1.880, 3:1.023, 4:0.729, 5:0.577, ...}
_D2 = {2:1.128, 3:1.693, 4:2.059, 5:2.326, ...}
_D3 = {2:0, ..., 7:0.076, 8:0.136, ...}
_D4 = {2:3.267, 3:2.574, 4:2.282, 5:2.114, ...}
# 공정 능력 지수
std_within = r_bar / d2
cp = (usl - lsl) / (6 * std_within)
cpu = (usl - mean) / (3 * std_within)
cpl = (mean - lsl) / (3 * std_within)
cpk = min(cpu, cpl)
# 관리도 한계
xbar_ucl = xbar_mean + a2 * r_bar
xbar_lcl = xbar_mean - a2 * r_bar
r_ucl = d4 * r_bar
r_lcl = d3 * r_bar
```
### `services/anova_service.py` — 핵심 계산
```python
from scipy import stats
f_stat, p_value = stats.f_oneway(*group_arrays)
significant = p_value < 0.05
# 그룹별 정규분포 곡선 (공통 x축 사용)
x_range = np.linspace(global_min, global_max, 200)
curve_y = stats.norm.pdf(x_range, mean, std)
```
### `routers/statistics.py` — SQL Injection 방지
```python
# 공정/항목 코드는 화이트리스트로만 허용
PROCESS_TABLE_MAP = {"P01": "insp_p01_cell_incoming", ...}
PROCESS_ITEM_MAP = {"P01": {"ocv_meas": "ocv_meas", ...}, ...}
def _get_col(process_code, item_code) -> str:
col = PROCESS_ITEM_MAP.get(process_code, {}).get(item_code)
if not col:
raise HTTPException(400, f"Unknown item_code")
return col
```
### CORS 설정
```python
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3003"],
allow_methods=["*"],
allow_headers=["*"],
)
```
### `requirements.txt`
```
fastapi>=0.110
uvicorn[standard]>=0.27
psycopg2-binary>=2.9
numpy>=1.26
scipy>=1.12
pandas>=2.2
python-dotenv>=1.0
httpx>=0.27
```
---
## 12. 실행 방법
### 백엔드
```bash
cd D:\Dev\03-2.statistical_analysis\src\backend
pip install -r requirements.txt
python -m uvicorn main:app --host 0.0.0.0 --port 8003 --reload
```
### 프론트엔드
```bash
cd D:\Dev\03-2.statistical_analysis\src\frontend
npm install
npm run dev
```
브라우저: `http://localhost:3003`
### 일괄 실행 (start.bat)
루트의 `start.bat` 더블클릭 시 백엔드 / 프론트엔드 각각 별도 터미널에서 자동 시작.
---
## 13. 비기능 요구사항
| 항목 | 내용 |
|---|---|
| 초기 날짜 범위 | `date_from=2026-06-01`, `date_to=2026-06-30` |
| 분석 실행 방식 | 수동 — 버튼 클릭 시에만 API 호출 (자동 실행 없음) |
| 로딩 상태 | 버튼 `분석 중...` 비활성 + 차트 영역 `Loading...` 텍스트 |
| 초기 상태 | `조건을 설정한 후 분석 실행 버튼을 클릭하세요` 안내 문구 |
| 상태 유지 | `localStorage` — 탭 전환 / 새로고침 후에도 필터·선택값 유지 |
| SQL Injection 방지 | 공정·항목 코드 화이트리스트 검증, 모든 필터 파라미터는 psycopg2 바인딩 |
| 타입 안전성 | TypeScript strict 모드 |
| 최대 데이터 포인트 | scatter 엔드포인트 `max_points=2000` (LIMIT 절 사용) |
---
## 14. 앞으로의 구현 계획
### Phase 2 — 높은 우선순위 (★★★)
| 기능 | 설명 | 비고 |
|---|---|---|
| **Pareto Chart** | 불량 항목·공정별 빈도 순위, 80/20 누적 기여율 시각화 | 품질 관리 필수 도구 |
| **Box Plot** | 제품별·라인별 분포를 상자 수염 그림으로 비교 | ANOVA 시각적 보완 |
| **I-MR Chart** | 개별값(Individual) + 이동범위(Moving Range) 관리도 | 서브그룹 구성이 어려운 공정 대응 |
| **정규성 검정** | Anderson-Darling 검정 + 정규 확률도(Probability Plot) | Cpk 산출의 전제 조건 확인 |
### Phase 3 — 중간 우선순위 (★★)
| 기능 | 설명 | 비고 |
|---|---|---|
| **Run Chart** | 시계열 추세·런(run) 검정 (연속 상승/하락 탐지) | 시계열 경향 분석 |
| **다중 회귀** | 여러 X → 1 Y 영향 분석, 편회귀계수 · VIF 출력 | 상관분석 탭 확장 |
| **Process Capability Sixpack** | Cpk·히스토그램·관리도·정규성을 한 화면에 통합 | Minitab Sixpack 동등 기능 |
### Phase 4 — 낮은 우선순위 (구현 복잡)
| 기능 | 설명 | 비고 |
|---|---|---|
| **Gage R&R** | 측정 시스템 분석 — 반복성(Repeatability) · 재현성(Reproducibility) | MSA 요구 시 구현 |
| **DOE** | 실험 계획법 — 요인 배치, 주효과·교호작용 분석 | |
| **Weibull 분석** | 신뢰성·수명 분포 분석, 고장률 예측 | |
---
## 15. 화면 참조 이미지
| 화면 | 경로 |
|---|---|
| Cpk Analysis | `docs\UI\Cpk Analysis.png` |
| ANOVA | `docs\UI\ANOVA.png` |
| Scatter Anomaly | `docs\UI\Scatter Anomaly.png` |
| Scatter Correlation | `docs\UI\Scatter Correlation.png` |
직접 따라 해보실 수 있도록 9개 공정의 검사 측정 데이터를 담은 샘플도 함께 올려둡니다. 압축을 풀어 PostgreSQL에 적재하면, 위 PRD의 테이블 구조 그대로 도구를 돌려볼 수 있습니다.
4-1. Cpk Analysis — 공정 능력과 관리도
첫 번째 탭은 공정 능력 분석입니다. Cpk는 공정이 규격(LSL~USL) 안에서 얼마나 안정적으로, 그리고 중심을 잘 맞춰 생산되는지를 하나의 숫자로 압축한 지표입니다. 합불 판정만으로는 보이지 않는 "이 공정이 앞으로도 규격을 지킬 여력이 있는가"를 읽어내는 도구입니다.

상단 KPI는 여섯 개입니다. N, Mean, σ Within에 더해 Cp · Cpk · Ppk를 보여줍니다. Cp는 공정의 잠재 능력(산포만 본 값), Cpk는 중심 치우침까지 반영한 실제 능력입니다. 둘의 차이가 크다면 산포는 괜찮은데 중심이 한쪽으로 쏠려 있다는 뜻입니다. Cpk와 Ppk는 값에 따라 색이 바뀌도록 했습니다. 1.33 이상이면 파란색, 1.0 미만이면 빨간색이라 화면을 켜는 순간 위험 공정이 눈에 들어옵니다.
차트는 2×2로 배치했습니다. 좌상단은 측정 빈도 히스토그램에 정규분포 곡선을 겹친 그림으로, LSL·USL을 붉은 점선으로 표시해 규격 대비 분포가 어디에 걸쳐 있는지 보여줍니다. 우상단은 n·Mean·σ·Cp·Cpk 등을 모은 요약 표입니다. 하단의 X-bar 관리도와 R 관리도는 서브그룹의 평균과 범위가 관리 한계(UCL·LCL) 안에서 움직이는지를 추적합니다. 서브그룹 기준은 고정(5개)·일별·주차별·월별로 바꿀 수 있습니다.
4-2. ANOVA — 그룹 간 평균 차이 검정
두 번째 탭은 일원분산분석(One-way ANOVA)입니다. "라인1이 라인2보다 평균이 높아 보인다", "이 제품군만 유독 값이 다르다" 같은 인상을 통계적으로 확인합니다. 눈으로 보면 달라 보여도 그게 우연한 산포인지, 의미 있는 차이인지는 검정을 해봐야 알 수 있습니다.

KPI는 F-Statistic, p-value, Significance, 그리고 자유도 두 개입니다. 판단의 핵심은 p-value입니다. 0.05보다 작으면 그룹 간 평균 차이가 우연으로 보기 어렵다는 뜻이고, 이때 Significance에 YES★가 빨갛게 뜹니다. 위 예시는 p = 0.005로, 제품군 간 평균에 유의한 차이가 있다고 판정된 경우입니다.
아래 차트는 그룹별 정규분포 곡선을 한 좌표 위에 겹쳐 그립니다. 곡선이 서로 어긋나 있을수록 그룹 간 차이가 크다는 것을 시각적으로 보여줍니다. 헤더의 색상 pill을 눌러 특정 그룹만 켜고 끌 수 있어, 어느 그룹이 튀는지 비교하기 편합니다. 그 아래 표에는 그룹별 N·평균·표준편차·사분위수가 정리됩니다.
4-3. Scatter · Anomaly — 2σ / 3σ 이상치 탐지
세 번째 탭은 측정값을 검사 순서대로 늘어놓고 이상치를 잡아내는 화면입니다. 관리도가 서브그룹 단위로 본다면, 이쪽은 개별 측정값 하나하나를 시간 축 위에 흩뿌려 보여줍니다.

평균에서 얼마나 벗어났는지에 따라 점의 색이 갈립니다. 2σ 안쪽은 파란색(Normal), 2σ와 3σ 사이는 주황색(Warning), 3σ를 넘으면 빨간색(Anomaly)입니다. μ와 ±2σ·±3σ는 점선으로 그어두어, 어느 점이 어느 밴드를 넘었는지 한눈에 보입니다. KPI에는 전체 포인트 수, 평균, 표준편차와 함께 3σ를 넘은 이상치 개수와 비율이 표시됩니다. 위 예시에서는 1,980개 중 5개(0.3%)가 이상치로 잡혔습니다. 통계량 계산과 2σ/3σ 분류는 raw 데이터를 받아 프론트에서 처리하도록 했습니다.
4-4. Scatter · Correlation — 상관분석과 회귀
마지막 탭은 두 측정 항목 사이의 관계를 봅니다. "OCV가 나빠지면 ΔOCV도 같이 움직이나?" 같은 질문에 정량적으로 답하는 화면입니다. 한 항목으로 다른 항목을 예측할 수 있는지, 둘 사이에 실제 관계가 있는지를 확인합니다.

KPI는 데이터 수, Pearson 상관계수 r, 결정계수 R², p-value, 그리고 관계의 강도를 말로 풀어준 Correlation입니다. r은 절댓값이 0.7을 넘으면 빨강, 0.4를 넘으면 노랑으로 표시해 관계의 세기를 직관적으로 보여줍니다. 위 예시는 r = 0.71, p < 0.001로 강한 양의 상관(Strong Positive)으로 판정되었습니다. 차트에는 산점도 위에 붉은 점선 회귀선을 얹고, 헤더에 회귀식과 r 값을 함께 적어 두 변수의 관계를 한 줄로 요약합니다.
네 개의 탭으로 공정 능력, 그룹 비교, 이상치 탐지, 상관분석까지 — 현장 품질 분석에 자주 쓰는 통계 도구를 한 자리에 모았습니다. 상용 통계 패키지가 해주던 일의 상당 부분을, 잘 정리된 PRD 한 장과 코딩 에이전트만으로 직접 만들 수 있다는 점이 이번 챕터의 핵심입니다. 그것도 통계 공식을 손수 코딩하지 않고, "무엇을 분석하고 싶은지"를 전달하는 데서 출발해서 말입니다.
PRD 14절에 적어둔 Phase 2 항목들 — Box Plot, I-MR 관리도, 정규성 검정, 파레토 등 — 은 이 글에서 다루지 않았습니다. 사실 이 부분은 따로 떼어, 나중에 "미니탭(Minitab)을 Claude Code로 구현하기"라는 주제의 별도 연재에서 본격적으로 풀어볼 생각입니다. 상용 통계 패키지가 제공하는 분석들을 하나씩 코딩 에이전트로 옮겨보는, 이 시리즈와는 또 다른 결의 작업이 될 것 같습니다.
그 이야기는 다음 기회로 미뤄두겠습니다. 우선 이번 챕터에서 만든 네 개의 탭만으로도, 잘 정리된 PRD 한 장과 코딩 에이전트가 현장 통계 분석을 어디까지 대신해줄 수 있는지는 충분히 보셨으리라 생각합니다.
'비전공자의 바이브 코딩 > 생산공정 AI 엔지니어링' 카테고리의 다른 글
| Chpater 5. 바이브 코딩으로 AI 도입하기(2) (0) | 2026.06.27 |
|---|---|
| Chpater 5. 바이브 코딩으로 AI 도입하기(1) (0) | 2026.06.19 |
| Chapter 3. 클로드코드(Claude Code)로 대시보드(Dashboard) 만들기 (0) | 2026.06.16 |
| Chapter 2. 무엇을 만들까? (0) | 2026.06.12 |
| Chapter 1. 공정 엔지니어가 코딩 에이전트를 배워야 하는 이유 (0) | 2026.06.07 |