공정 엔지니어의 AI 엔지니어로의 성장 기록

비전공자의 바이브 코딩/생산공정 AI 엔지니어링

Chapter 3. 클로드코드(Claude Code)로 대시보드(Dashboard) 만들기

ai-process-engineer 2026. 6. 16. 23:30

Chapter 1에서는 공정 엔지니어가 코딩 에이전트를 배워야 하는 이유를, Chapter 2에서는 우리가 현장에서 다루는 데이터로 무엇을 만들 수 있는지를 이야기했습니다. 이번 Chapter 3부터는 직접 손을 움직입니다. 첫 결과물은 생산 라인의 불량 데이터를 시각화하는 대시보드입니다.

이 대시보드는 이전에 정리했던 IT 외주 없이 직접 만든 제조 품질 분석 시스템의 풀스택 흐름을 따릅니다. FastAPI로 집계하고, Next.js와 Recharts로 그리는 구조입니다.

한 가지만 미리 밝혀두겠습니다. 이번 프로젝트에서는 DB를 쓰지 않습니다. 설비에서 떨어진 불량 이력 CSV 한 장을 백엔드가 직접 읽어 메모리에 올려두고, 필터 조건에 맞춰 pandas로 집계해 REST API로 내려줄 뿐입니다. 다만 이는 설명의 편의를 위한 선택입니다. 마이그레이션이나 ORM, 스키마 같은 곁가지를 덜어내고 차트를 그리는 핵심 흐름에만 집중하기 위해서입니다. 위 글에서 다룬 실제 시스템에서는 DB를 정식으로 구성해 데이터를 적재·조회합니다. 즉, 운영 환경이라면 CSV 대신 DB가 들어오는 자리라고 보시면 됩니다. 학습 단계에서 가볍게 띄워보기에는 이 정도로 충분합니다.

화면은 두 개입니다. 상단의 토글 스위치로 전환합니다.

  • Defect Trend — 기간별 불량 건수의 추이를 꺾은선으로 봅니다.
  • Defect Share — 불량 항목별 점유율을 도넛 파이차트와 파레토 차트로 봅니다.

여기서 한 가지 짚고 넘어가고 싶습니다. 아래에 붙인 PRD는 제가 처음부터 직접 쓴 것이 아닙니다. 제가 Claude Code에 건넨 것은 몇 가지뿐이었습니다. 만들고 싶은 목표, 사용할 기술 스택, 데이터(CSV)에 대한 설명, 프론트·백엔드 포트, 원하는 기능, 그리고 화면 느낌을 담은 예시 이미지 몇 장. 딱 그 정도의 거친 요구사항이었습니다.

그러면 Claude Code가 이를 바탕으로 아키텍처, 프로젝트 폴더 구조, 페이지 구성, API·컴포넌트 명세, 백엔드 집계 로직까지 스스로 검토하고 설계해 아래와 같은 PRD로 정리해줬습니다. 저는 그 결과물을 읽어보고 몇 군데만 다듬었을 뿐입니다. 다시 말해, 제 역할은 "무엇을, 어떤 느낌으로 원하는가"를 분명히 전달하는 데까지였고, 그것을 정식 설계 문서로 구체화하는 일조차 상당 부분을 Claude Code가 맡았습니다.

아래 PRD 전문을 그대로 복사해 Claude Code에 건네면 같은 대시보드를 재현할 수 있습니다. 화면 구성, API 명세, 디자인 토큰, 백엔드 집계 로직까지 모두 담겨 있습니다.


PRD — Defect Dashboard 전문

아래 블록을 통째로 복사해 프로젝트 루트의 PRD.md로 저장하고, Claude Code에게 "이 PRD대로 만들어줘"라고 요청하면 됩니다. 물론 앞서 말씀드린 것처럼, 이 완성된 PRD를 그대로 쓰지 않고 여러분의 목표·데이터·원하는 화면 느낌만 정리해 건넨 뒤 "이 요구사항으로 PRD부터 작성해줘"라고 시켜도 됩니다. 어느 쪽이든 출발점은 결국 "무엇을 원하는가"입니다.

저는 한 번에 다 시키지 않고 백엔드 → 프론트 공통 레이아웃 → Trend → Share 순으로 끊어 진행했습니다. 중간에 화면을 확인하며 색상과 레이블 위치만 몇 번 손봤습니다.

PRD 전문 펼치기 / 접기 (클릭)
# PRD — Defect Dashboard (Trend & Share)

**작성일**: 2026-06-16
**버전**: 1.0

---

## 1. 개요

생산 라인에서 발생하는 불량 데이터를 시각화하는 대시보드.
상단 토글 스위치로 **Defect Trend** / **Defect Share** 두 화면을 전환한다.
사이드바 없음. DB 없음. CSV 파일을 백엔드에서 직접 읽어 REST API로 제공한다.

---

## 2. 목표

- 생산 라인 불량 건수의 시계열 추이를 꺾은선 그래프로 파악
- 불량 항목별 점유율을 도넛 파이차트로 파악
- 상위 불량 항목의 누적 기여율을 파레토 차트로 파악
- Trend 차트에서 특정 기간 더블클릭 시 해당 기간으로 Share 화면 자동 드릴다운

---

## 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 | pandas | ^2.2 |
| DBMS | — | 미사용 (CSV 직접 파싱) |

### 포트

| 서비스 | URL |
|---|---|
| Frontend | `http://localhost:3010` |
| Backend | `http://localhost:8010` |

---

## 4. 데이터 소스

**파일 경로**: `data/defects_2026.csv`

### 컬럼 정의

| 컬럼명 | 타입 | 설명 | 예시 |
|---|---|---|---|
| `date` | string (YYYY-MM-DD) | 불량 발생일 | `2026-04-01` |
| `line` | string | 생산 라인 코드 | `Line01` ~ `Line05` |
| `product` | string | 제품명 | `BM-Alpha 100` |
| `process` | string | 공정 코드 | `P01` ~ `P10` |
| `item` | string | 불량 항목명 | `DCIR`, `OCV`, `Bolt_Angle` … |
| `barcode` | string | 제품 바코드 (1행 = 1건) | `BM-20260401-L04-...` |
| `defect_count` | integer | 불량 수량 (항상 1) | `1` |

### 마스터 데이터 (CSV에서 동적 추출)

- **Products**: `BM-Alpha 100`, `BM-Beta 200`, `BM-Gamma 300`, `BM-Delta 400`, `BM-Epsilon 500`
- **Lines**: `Line01` ~ `Line05`
- **Processes**: `P01` ~ `P10`
- **Defect Items**: `DCIR`, `OCV`, `ACIR`, `Bolt_Angle`, `Bolt_Torque`, `Tab_Length`, `Tab_Protrusion_Front`, `Tab_Protrusion_Rear`, `SDR`, `Balancing`, `Vision_NG`, `Insul_Resist`, `BendLen_Front` 등

---

## 5. 프로젝트 구조

```
D:\Dev\03-1.trend&pie_chart\
├── data\
│   └── defects_2026.csv
├── docs\
│   ├── PRD.md
│   ├── Defect Trend.png          # 화면 디자인 참조
│   └── Defect Share.png          # 화면 디자인 참조
├── src\
│   ├── backend\                  # FastAPI 앱
│   │   ├── main.py
│   │   ├── requirements.txt
│   │   ├── routers\
│   │   │   ├── __init__.py
│   │   │   ├── master.py         # GET /api/master/*
│   │   │   └── dashboard.py      # GET /api/dashboard/*
│   │   └── services\
│   │       ├── __init__.py
│   │       └── csv_loader.py     # CSV 로딩 및 필터링
│   └── frontend\                 # Next.js 앱
│       ├── app\
│       │   ├── globals.css
│       │   ├── layout.tsx
│       │   └── page.tsx          # 단일 페이지 (토글로 두 화면 전환)
│       ├── components\
│       │   ├── FilterBar.tsx
│       │   ├── Card.tsx          # Card + KpiCard
│       │   ├── TrendView.tsx     # Defect Trend 화면
│       │   └── ShareView.tsx     # Defect Share 화면
│       ├── lib\
│       │   ├── api.ts            # fetch 래퍼
│       │   └── hooks.ts          # useStoredState
│       ├── package.json
│       ├── tsconfig.json
│       ├── next.config.ts
│       └── postcss.config.mjs
└── start.bat                     # 백엔드/프론트엔드 동시 실행
```

---

## 6. 아키텍처

```
[ Browser :3010 ]
       |
       | fetch (no-store)
       |
[ Next.js Frontend ]  ──── REST API ──── [ FastAPI Backend :8010 ]
                                                   |
                                        pandas lru_cache 메모리 로드
                                        data/defects_2026.csv
```

- 백엔드 시작 시 CSV를 pandas DataFrame으로 1회 로드 → `lru_cache`로 캐시
- 필터 조건(날짜, 라인, 제품)은 Python DataFrame 연산으로 처리
- DB 없음 — 마이그레이션, ORM 불필요

---

## 7. 화면 구성

### 7.1 공통 레이아웃

```
┌────────────────────────────────────────────────────────────────┐
│  Dashboard  Defect Trend                  [ Defect Trend | Defect Share ] │
│  (부제목)                                                        │
├────────────────────────────────────────────────────────────────┤
│  FILTER  [All Products ▼] [All Lines ▼] [날짜 ~] [옵션 select] │
├────────────────────────────────────────────────────────────────┤
│  KPI Card 1     │     KPI Card 2     │     KPI Card 3          │
├────────────────────────────────────────────────────────────────┤
│                    차트 영역 (뷰별 상이)                         │
└────────────────────────────────────────────────────────────────┘
```

**공통 규칙**
- 사이드바 없음 — 전체 너비 사용
- 토글 스위치: Header 우측 pill 형태, Defect Trend / Defect Share 전환
- URL 이동 없이 `view` state로 `TrendView` / `ShareView` 조건부 렌더링
- 각 뷰의 필터 상태는 `localStorage`에 독립 저장 (뷰 전환 시 유지)
- 배경색: `#f8fafc` (`--bg-primary`)

---

### 7.2 Page 1 — Defect Trend

#### Title bar
- 제목: `Dashboard` (light) + `Defect Trend` (bold 900)
- 부제목: `Assembly line defect count over time · double-click a point to drill into Defect Share`

#### FilterBar 컨트롤

| 컨트롤 | 옵션 | 기본값 |
|---|---|---|
| Product 드롭다운 | All Products + 제품별 | All Products |
| Line 드롭다운 | All Lines + Line01~Line05 | All Lines |
| Date From | `<input type="date">` | 2026-01-01 |
| Date To | `<input type="date">` | 2026-06-30 |
| 집계 단위 | Daily / Weekly / Monthly | Monthly |
| 분할 기준 | Total / By Line / By Product | Total |

#### KPI Cards

| 라벨 | 값 | 색상 |
|---|---|---|
| TOTAL DEFECTS | 필터 범위 내 총 불량 합산 | 기본 |
| PERIODS | 집계 기간 수 | 기본 |
| PEAK PERIOD | 불량 최다 기간 | `#dc2626` (빨강) |

#### 꺾은선 차트 (`LineChart`)

- **차트 카드 헤더** (다크 `#1e2530`): `Defect Count Trend` / `Monthly · Total`
- **헤더 우측 컨트롤**:
  - `Labels` 토글 버튼 — ON 시 각 데이터 포인트 위에 수치 표시
  - 시리즈별 pill 버튼 — 클릭으로 개별 라인 표시/숨김
- **X축**: period (날짜/주/월), `-35°` 회전
- **Y축**: 불량 건수, `niceMin` ~ `niceMax` 자동 범위
- **라인 스타일**: `strokeWidth: 2`, dot `r: 4.5` (테두리 흰색 2px), activeDot `r: 6.5`
- **색상**: `["#fb923c", "#2563eb", "#16a34a", "#dc2626", "#7c3aed"]`

#### splitBy 피벗 로직

| splitBy | 동작 |
|---|---|
| `total` | 단일 API 호출 → 단일 시리즈 |
| `line` | Line01~05 각각 병렬 API 호출 → period 기준 pivot |
| `product` | 제품별 각각 병렬 API 호출 → period 기준 pivot |

#### 더블클릭 드릴다운

1. 차트 클릭 시 `period` + timestamp 기록
2. 500ms 이내 동일 period 재클릭 → 더블클릭 판정
3. `periodToRange()` 로 날짜 범위 계산 (day/week/month 각각 처리)
4. `sessionStorage("share_override_filters")` 에 필터 저장
5. `view` state를 `"share"` 로 전환

---

### 7.3 Page 2 — Defect Share

#### Title bar
- 제목: `Dashboard` (light) + `Defect Share` (bold 900)
- 부제목: `Defect distribution by category`

#### FilterBar 컨트롤

| 컨트롤 | 옵션 | 기본값 |
|---|---|---|
| Product 드롭다운 | All Products + 제품별 | All Products |
| Line 드롭다운 | All Lines + Line01~Line05 | All Lines |
| Date From | `<input type="date">` | 2026-01-01 |
| Date To | `<input type="date">` | 2026-06-30 |
| 분류 기준 | By Product / By Line / By Process / By Defect Item | By Defect Item |

#### KPI Cards

| 라벨 | 값 |
|---|---|
| TOTAL DEFECTS | 전체 불량 합계 |
| TOP-10 DEFECTS | 상위 10개 항목 불량 합계 |
| CATEGORIES | 전체 카테고리 수 |

#### 도넛 파이차트 (`PieChart`) — 좌측 1/2

- **헤더**: `Defect Share` / `Proportion of total defects (top 10)`
- `outerRadius="42%"`, `innerRadius="22%"`, `paddingAngle={2}`
- **외부 레이블**: 항목명 / 건수 / 점유율% (짧은 리더선 연결), 4% 미만 숨김
- **하단 범례** (`<Legend>`)
- **색상**: `["#fb923c","#2563eb","#16a34a","#dc2626","#7c3aed","#0891b2","#d97706","#be185d","#059669","#9333ea"]`

#### 파레토 차트 (`ComposedChart`) — 우측 1/2

- **헤더**: `Pareto Chart` / `Defect count + cumulative % (top 10)`
- **좌측 Y축** (`yAxisId="bar"`): 불량 건수
- **우측 Y축** (`yAxisId="line"`): 누적 % (domain `[0, 100]`, `tickFormatter: v => v%`)
- **Bar**: 상위 10개, `radius=[4,4,0,0]`, 항목별 `COLORS` 순서 적용, 상단 수치 레이블
- **Line**: 누적 %, `stroke: "#374151"`, `strokeWidth: 2`, dot `r: 3.5`
- **X축**: 항목명, `-35°` 회전, `interval: 0`

#### 드릴다운 수신

- `ShareView` 마운트 시 `sessionStorage("share_override_filters")` 확인
- 값이 있으면 해당 필터를 초기 상태로 적용 → sessionStorage 즉시 삭제
- `localStorage("share_filters")` 에도 동기화 (이후 재방문 시 유지)

---

## 8. API 명세

### Base URL
`http://localhost:8010/api`

---

### `GET /api/master/products`

**응답**
```json
[
  { "code": "BM-Alpha 100", "name": "BM-Alpha 100" },
  { "code": "BM-Beta 200",  "name": "BM-Beta 200"  }
]
```

---

### `GET /api/master/lines`

**응답**
```json
[
  { "code": "Line01", "name": "Line01" },
  { "code": "Line02", "name": "Line02" }
]
```

---

### `GET /api/dashboard/trend`

| Query Param | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `product` | string | — | 제품 필터 (`BM-Alpha 100` 등) |
| `line` | string | — | 라인 필터 (`Line01` 등) |
| `date_from` | string | — | 시작일 (YYYY-MM-DD) |
| `date_to` | string | — | 종료일 (YYYY-MM-DD) |
| `granularity` | string | `day` | `day` / `week` / `month` |

**응답**
```json
[
  { "period": "2026-03", "total_defects": 1284 },
  { "period": "2026-04", "total_defects": 1102 }
]
```

**period 포맷**
- `day`: `"2026-03-16"`
- `week`: `"2026-03-16"` (해당 주 월요일 날짜)
- `month`: `"2026-03"`

---

### `GET /api/dashboard/share`

| Query Param | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `product` | string | — | 제품 필터 |
| `line` | string | — | 라인 필터 |
| `date_from` | string | — | 시작일 |
| `date_to` | string | — | 종료일 |
| `group_by` | string | `item` | `product` / `line` / `process` / `item` |

**응답** (내림차순 정렬, 전체 반환 → 프론트에서 top 10 슬라이싱)
```json
[
  { "label": "DCIR",      "total_defects": 820, "share_pct": 15.09 },
  { "label": "Bolt_Angle","total_defects": 610, "share_pct": 11.21 }
]
```

---

## 9. 컴포넌트 명세

### `app/layout.tsx`
- 사이드바 없음, `<body>` 전체 너비 사용
- `height: 100vh`, `overflow: hidden`, `flex-direction: column`
- Inter 폰트 (Google Fonts), `globals.css` 전역 적용

### `app/page.tsx`
- `view: "trend" | "share"` state 관리
- Header (title + 토글 스위치) 렌더
- `view === "trend"` → `<TrendView onDrillDown={...} />`
- `view === "share"` → `<ShareView />`

### `components/FilterBar.tsx`
Props: `products`, `lines`, `filters`, `onChange`, `extra`
- Product / Line 드롭다운, Date From/To 입력, `extra` slot (뷰별 추가 select)
- inline style, 흰 배경 + `--border` 테두리, border-radius 10

### `components/Card.tsx`
- `Card`: 다크 헤더(`--sidebar-bg`) + 콘텐츠 영역
- `KpiCard`: 라벨(10px uppercase) + 수치(22px bold 900) + 서브텍스트(선택)

### `components/TrendView.tsx`
- 내부 상태: `granularity`, `splitBy`, `filters`, `chartData`, `seriesKeys`, `hiddenSeries`, `showLabels`
- `filters` / `granularity` / `splitBy` → `localStorage` 유지 (`useStoredState`)
- 더블클릭 감지: `useRef`로 마지막 클릭 period + timestamp 기록

### `components/ShareView.tsx`
- 내부 상태: `filters`, `groupBy`, `data`
- 마운트 시 `sessionStorage` 드릴다운 필터 우선 적용
- `top10 = data.slice(0, 10)` — 파이차트 및 파레토 공통 사용
- 파레토: `[...top10].sort(desc).map()` 으로 누적 % 계산

### `lib/api.ts`
- Base URL: `http://localhost:8010/api`
- `get<T>(path, params)` — URL 파라미터 자동 직렬화, undefined 값 제외
- 노출 메서드: `api.products()`, `api.lines()`, `api.trend(p)`, `api.share(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;
  --sidebar-active-bg:   rgba(251,146,60,0.15);
  --sidebar-active-text: #fb923c;
  --sidebar-border:      rgba(255,255,255,0.07);
}
```

### 차트 색상 팔레트

```ts
// Trend 꺾은선 (최대 5개 시리즈)
const LINE_COLORS = ["#fb923c", "#2563eb", "#16a34a", "#dc2626", "#7c3aed"];

// Share 파이 / 파레토 (최대 10개 항목)
const COLORS = [
  "#fb923c","#2563eb","#16a34a","#dc2626","#7c3aed",
  "#0891b2","#d97706","#be185d","#059669","#9333ea",
];
```

### 스타일 원칙

- **inline style 우선** — Tailwind는 `globals.css` `@import "tailwindcss"` 선언만 사용
- 차트 라이브러리: **Recharts 고정** (Chart.js, D3 사용 안 함)
- 색상: 위 팔레트 외 임의 색상 추가 금지
- 스크롤바: `::-webkit-scrollbar` 커스텀 (width 5px, `#cbd5e1`)

---

## 11. 백엔드 구현 상세

### `services/csv_loader.py`

```python
from functools import lru_cache
from pathlib import Path
import pandas as pd

CSV_PATH = Path(__file__).parents[3] / "data" / "defects_2026.csv"

@lru_cache(maxsize=1)
def load_df() -> pd.DataFrame:
    return pd.read_csv(CSV_PATH, parse_dates=["date"])

def apply_filters(df, product=None, line=None, date_from=None, date_to=None):
    if product:    df = df[df["product"] == product]
    if line:       df = df[df["line"]    == line]
    if date_from:  df = df[df["date"] >= pd.Timestamp(date_from)]
    if date_to:    df = df[df["date"] <= pd.Timestamp(date_to)]
    return df
```

- `CSV_PATH`: `__file__` 기준 상대 경로 → 실행 위치 무관
- `lru_cache(maxsize=1)`: 서버 프로세스 내 단일 인스턴스 캐시

### `routers/dashboard.py` — Trend 집계

```python
if granularity == "month":
    df["period"] = df["date"].dt.strftime("%Y-%m")
elif granularity == "week":
    # 해당 주의 월요일을 period로 사용
    df["period"] = (df["date"] - pd.to_timedelta(df["date"].dt.weekday, unit="D")).dt.strftime("%Y-%m-%d")
else:
    df["period"] = df["date"].dt.strftime("%Y-%m-%d")

result = df.groupby("period")["defect_count"].sum().reset_index()
result.columns = ["period", "total_defects"]
return result.sort_values("period").to_dict(orient="records")
```

### `routers/dashboard.py` — Share 집계

```python
col_map = {"product":"product", "line":"line", "process":"process", "item":"item"}
col = col_map[group_by]
total = int(df["defect_count"].sum())

result = df.groupby(col)["defect_count"].sum().reset_index()
result.columns = ["label", "total_defects"]
result["share_pct"] = (result["total_defects"] / total * 100).round(2) if total > 0 else 0.0
return result.sort_values("total_defects", ascending=False).to_dict(orient="records")
```

### CORS 설정

```python
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3010"],
    allow_methods=["*"],
    allow_headers=["*"],
)
```

### `requirements.txt`

```
fastapi>=0.110
uvicorn[standard]>=0.27
pandas>=2.2
python-dotenv>=1.0
```

---

## 12. 프론트엔드 실행 스크립트 이슈

프로젝트 경로에 `&` 문자(`03-1.trend&pie_chart`)가 포함되어 있어 Windows CMD에서 npm 스크립트 실행 시 경로가 잘려 `next` 바이너리를 찾지 못하는 문제가 발생한다.

**해결**: `package.json`의 스크립트에서 `next` 대신 `node node_modules/next/dist/bin/next` 로 직접 호출.

```json
"scripts": {
  "dev":   "node node_modules/next/dist/bin/next dev -p 3010",
  "build": "node node_modules/next/dist/bin/next build",
  "start": "node node_modules/next/dist/bin/next start -p 3010"
}
```

---

## 13. 실행 방법

### 백엔드

```bash
cd D:\Dev\03-1.trend&pie_chart\src\backend
pip install -r requirements.txt
python -m uvicorn main:app --reload --port 8010
```

### 프론트엔드

```bash
cd D:\Dev\03-1.trend&pie_chart\src\frontend
npm install
npm run dev
```

브라우저: `http://localhost:3010`

### 일괄 실행 (start.bat)

루트의 `start.bat` 더블클릭 시 백엔드 / 프론트엔드 각각 별도 터미널에서 자동 시작.

---

## 14. 비기능 요구사항

| 항목 | 내용 |
|---|---|
| 초기 날짜 범위 | `date_from=2026-01-01`, `date_to=2026-06-30` |
| 로딩 상태 | 차트 영역에 `Loading...` 텍스트 표시 |
| 필터 반응성 | 변경 즉시 API 재호출 (debounce 불필요) |
| 상태 유지 | `localStorage` — 뷰 전환 / 새로고침 후에도 필터·옵션 유지 |
| 드릴다운 전달 | `sessionStorage` — 뷰 간 일회성 필터 전달 후 즉시 삭제 |
| 타입 안전성 | TypeScript strict 모드 |

---

## 15. 화면 참조 이미지

| 화면 | 경로 |
|---|---|
| Defect Trend | `docs\Defect Trend.png` |
| Defect Share | `docs\Defect Share.png` |

바로 따라 해보실 수 있도록 실습용 샘플 불량 데이터 파일도 함께 올려둡니다. 내려받아 프로젝트의 data/defects_2026.csv 위치에 두면 됩니다.

defects_2026.csv
776.1 kB


3-1. Trend 그래프 그리기

먼저 Defect Trend 화면입니다. 가장 위에는 세 개의 KPI 카드가 있습니다. 필터 범위 안의 총 불량 수, 집계 기간 수, 그리고 불량이 가장 많았던 기간(Peak Period)입니다. Peak Period만 빨간색으로 강조해, 화면을 켜자마자 "언제가 문제였는지"가 먼저 눈에 들어오도록 했습니다.

차트 본체는 Recharts의 LineChart 하나입니다. 단순해 보이지만 필터 조합에 따라 동작이 달라집니다. 상단 필터에서 제품·라인·기간을 고르고, 집계 단위를 일/주/월로, 분할 기준을 Total·By Line·By Product로 바꿀 수 있습니다.

여기서 핵심은 분할 기준(splitBy) 처리입니다. Total이면 API를 한 번 호출해 단일 선을 그리지만, By Line이면 Line01~05를 각각 병렬로 호출한 뒤 기간(period)을 기준으로 피벗해 다섯 개의 선을 겹쳐 그립니다. 이런 분기 로직은 직접 짜려면 번거롭지만, PRD에 "splitBy 피벗 로직" 표로 동작을 명시해두니 Claude Code가 의도대로 구현했습니다.

이 화면에서 가장 마음에 드는 기능은 더블클릭 드릴다운입니다. 추이를 보다가 유독 튀는 달이 보이면, 그 점을 더블클릭하는 것만으로 해당 기간이 필터에 적용된 채 Defect Share 화면으로 넘어갑니다. "3월에 왜 이렇게 많았지?"라는 질문에서 "그럼 그 달엔 어떤 항목이 문제였지?"로 자연스럽게 이어지도록 만든 것입니다. 클릭 시각을 기록해 500ms 안에 같은 점을 다시 누르면 더블클릭으로 판정하고, 기간을 날짜 범위로 환산해 sessionStorage에 담아 화면을 전환합니다.


3-2. pie 차트, Pareto 차트 그리기

Defect Share 화면은 "무엇이 문제인가"에 답합니다. 좌측에는 항목별 점유율을 보여주는 도넛 파이차트, 우측에는 파레토 차트를 나란히 배치했습니다.

파이차트는 상위 10개 항목의 점유율을 도넛 형태로 그립니다. 항목명·건수·점유율을 외부 레이블로 짧은 리더선과 함께 표시하되, 점유율 4% 미만은 레이블을 숨겨 조각이 작은 항목끼리 글자가 겹치지 않도록 했습니다. 분류 기준은 제품별·라인별·공정별·불량 항목별로 바꿀 수 있어, 같은 데이터를 보는 각도를 그때그때 바꿀 수 있습니다.

우측 파레토 차트가 실무에서는 더 유용합니다. 막대는 항목별 불량 건수(좌측 Y축), 그 위를 잇는 선은 누적 점유율(우측 Y축, 0~100%)입니다. 막대를 내림차순으로 세워두면, 상위 몇 개 항목이 전체 불량의 대부분을 차지하는지가 한눈에 보입니다. 이른바 80/20 법칙을 시각적으로 확인하는 도구이고, 개선 활동의 우선순위를 정할 때 가장 먼저 그리는 그림이기도 합니다. Recharts에서는 ComposedChart에 막대와 선을 함께 얹고, 좌우 두 개의 Y축을 따로 두어 구현했습니다.

앞서 Trend 화면에서 더블클릭으로 넘긴 필터는 이 화면이 마운트될 때 받습니다. sessionStorage에 값이 있으면 그 필터를 초기 상태로 적용하고, 다음 진입 때 다시 끌려오지 않도록 값을 즉시 지웁니다. 두 화면이 한 페이지 안에서 상태만 바꿔가며 맞물려 도는 구조입니다.


두 개의 화면, 다섯 개의 차트. 코드를 한 줄도 직접 타이핑하지 않았고, 그 코드의 설계도가 된 PRD조차 제가 처음부터 쓴 게 아니었습니다. 거친 요구사항 몇 줄과 예시 이미지에서 출발해, 아키텍처와 명세는 Claude Code가 채우고, 저는 방향을 정하고 결과를 검토하며 다듬었습니다. 물론 색이 마음에 안 들거나 레이블 위치가 어긋나는 부분은 화면을 보며 몇 번 손봐야 했습니다. 그래도 분명한 것은, 엔지니어가 끝까지 쥐고 있어야 할 것은 "무엇을, 왜 만드는가"이고, "어떻게 만드는가"의 상당 부분은 이제 코딩 에이전트에 맡길 수 있다는 점입니다.

이번 대시보드는 MES에서 떨어지는 집계 데이터를 다루는 가장 기본적인 형태였습니다. 다음 Chapter에서는 같은 데이터를 한 걸음 더 밀어붙여, 비가동 요인까지 반영한 OEE 자동 산출과 추이 분석으로 넘어가겠습니다.