# INVYONE Stage-2 — 개발 진행 상황

> 마지막 업데이트: 2026-04-30
> 목적: SI FLEX 베트남법인 EHS 방재센터 입찰 PT (5/8 14:00) 시연용 디지털 트윈 데모
> 위치: `C:\Users\defaultuser0\Downloads\invyone-stage2-demo-v1`

---

## 1. 프로젝트 개요

### 1.1 무엇을 만들고 있는가
수처리 플랜트 (전처리 → UF → RO → MBR → 후처리) SCADA 디지털 트윈 데모.
- **목표**: PT에서 발표자가 자유롭게 클릭하며 "이 시스템이 어떻게 제어되는지" 시각적으로 보여주는 인터랙티브 시연
- **철학**: 정해진 시나리오를 따라가는 데모 ❌ / 샌드박스형 자유 제어 데모 ⭕
  - 어떤 펌프든, 밸브든, 탱크든 클릭하면 즉시 반응
  - 250ms tick 루프가 매 순간 흐름·압력·알람을 자동 재계산

### 1.2 참고 자료
- 실제 사진: `C:\Users\defaultuser0\Downloads\참고\` 안 PDF/Excel
- 시작 자산: `C:\Users\defaultuser0\Downloads\scada-svg-kit-v1\` (SVG 부품 라이브러리)
- 추가 자산: `C:\Users\defaultuser0\Downloads\invyone-extra-svg\` (압력계, 카본 카트리지)
- Stage-1 데모: `C:\Users\defaultuser0\Downloads\invyone-stage1-demo-v1\` (런타임 패턴 참고)

---

## 2. 파일 구조

```
invyone-stage2-demo-v1/
├── index.html                        메인 페이지 (인라인 SVG 캔버스)
├── css/
│   └── invyone-stage2.css            SCADA 다크 테마 + 애니메이션
├── js/
│   ├── components.js                 SVG 컴포넌트 템플릿 라이브러리
│   ├── topology.js                   플랜트 노드 정의 + 포트 + Anchor + 자동 라우팅
│   ├── engine.js                     state + tick + 흐름/압력/알람 시뮬레이션
│   ├── ui.js                         클릭 위임 + 인스펙터 + 모드 버튼
│   └── main.js                       부트스트랩
├── svg/
│   ├── pressure-gauge.svg            (참조용 원본)
│   └── carbon-cartridge-bank.svg     (참조용 원본)
└── docs/
    ├── README.md                     사용자 가이드
    └── PROGRESS.md                   ← 이 파일
```

### 2.1 파일별 역할 요약

#### `index.html` (109 lines)
- 인라인 SVG 캔버스 (`viewBox="0 0 2000 880"`, preserveAspectRatio xMidYMid meet)
- 글로벌 SVG `<defs>`: metal-h, metal-v, metal-tank, glass-grad 그라디언트
- 사이드바: 모드 버튼 (NORMAL/BACKWASH/CIP/STOP/+CHEM-DOSE), Reset/Pause/Alarm/Flow Multiplier, Inspector
- 하단: Event Log 영역
- 5개 JS 파일을 순서대로 로드: components → topology → engine → ui → main

#### `css/invyone-stage2.css` (~280 lines)
- SCADA 다크 테마 변수: `--scada-bg`, `--scada-flow`, `--scada-active`, `--scada-warning`, `--scada-alarm` 등
- 컴포넌트 상태 클래스:
  - `.comp.comp-pump.running .pump-impeller` → 회전 애니메이션 (0.8s linear infinite)
  - `.comp.comp-valve.closed .valve-handle` → 90° 회전
  - `.flow.active` → stroke-dashoffset march (0.6s)
  - `.flow.blocked` → 빨간 깜빡임
  - `.comp.comp-tank.alarm-hh/.alarm-ll` → 펄스 알람
  - `.comp-module.active .cell-fluid` → fluidPulse (1.4s, opacity + scale)
  - `.comp-tank-mbr.aerating` → MBR 외곽 청록 발광 (aerationGlow)
  - `.mbr-bubbles .bubble` → 상승 + 페이드 (bubbleRise, 2.4s)

#### `js/components.js` (~400 lines)
SVG 컴포넌트 템플릿 함수 라이브러리. 각 함수는 SVG 문자열을 반환.
- `COMP.tank` — 큰 사각 탱크 (RAW, WATER FILTER, DP, REGULATING) — **측면 스케일 눈금 0/25/50/75/100 추가됨**
- `COMP.dosingTank` — 작은 도징 탱크 (CHEM, CEP-1/2, CIP TANK)
- `COMP.acTank` — SAND/AC 사일로 (콘 상단 + 솔리드 스커트 + 하단 outlet stub) — **A자 다리 제거, 솔리드 베이스로 변경됨**
- `COMP.pump` — 가로 원심 펌프 (좌측 인입, 우측 토출) — **모터 쿨링 핀 4개 + 흐름 방향 화살표 추가됨**
- `COMP.pumpV` — **세로 원심 펌프 (상단 인입, 우측 토출)** — SAND/AC/CHEM 펌프용 (탱크 바로 아래 정렬)
- `COMP.airBlower` — 에어 블로어 — **4 곡선 잎사귀 블레이드 (X자 → 진짜 fan 모양)**, LED 모터 본체 안쪽으로 이동
- `COMP.valve` — **자체 파이프 rect 제거** (하부 메인 파이프의 흐름이 밸브 영역에서 끊기지 않게). 본체 + 핸들 + flow-l/flow-r만
- `COMP.pipe` — **rect 기반 직각 덕트 + 자동 갭/점프 아치** (P&ID 표준 crossing 표시)
  - `points` 배열 받아서 segments + corners 자동 생성
  - `gaps` 배열 받으면 해당 위치에 18px 갭 + 곡선 점프 아치
  - flow line은 path with 반원 호 (A 명령)로 갭 위로 우회
- `COMP.pressureGauge` — 아날로그 다이얼 (0-max), 정상/주의/알람 3구간 호, 회전 바늘
- `COMP.sensor` — DO/Temp/TSS/pH 등 디지털 readout
- `COMP.led` — 작은 상태 LED
- `COMP.filter` — 단순 흰색 필터 통 (% 텍스트 없음)
- `COMP.membraneBank` — UF/RO 멤브레인 뱅크
  - `orientation: 'vertical'` → 5개 세로 술병 (UF) — 22w × 130h, 적색 캡 상하
  - `orientation: 'horizontal'` → 5개 가로 원통 적층 (RO) — 180w × 16h
- `COMP.carbonBank` — MBR 카본 카트리지 5개 (배경 투명 → MBR 액체 비침)
- `COMP.statusTile` — KPI 패널 (FLOW IN/OUT, RECOVERY, ACTIVE ALARMS)
- `COMP.panel` — 점선 외곽 박스 (CEP SYSTEM, CIP SYSTEM)

#### `js/topology.js` (~480 lines) — 핵심 파일
플랜트 위상학 + Anchor 시스템 + 자동 파이프 라우팅 + 교차 감지.

**상수 배열** (TANKS, PUMPS, VALVES, FILTERS, MODULES, GAUGES, SENSORS, LEDS, TILES, PANELS, PIPES)

**Anchor 시스템** (자세한 건 §3 참조):
- `getPortAbsPos(comp, portName)` — 컴포넌트 타입별 포트 절대좌표 계산
- `tagTypes()` — 각 컴포넌트에 `_type` 부여
- `pipePointAt(points, fraction)` — 직각 폴리라인의 fraction 지점 계산
- `resolveAnchors()` — 멀티패스 (최대 12회) Port-anchor 해결
- `resolveOnPipeAnchors(resolvedPipes)` — 파이프 위 컴포넌트 배치

**파이프 라우팅**:
- `resolveRef(ref, ports)` — `'comp.port'` 문자열을 절대 좌표로
- `autoRoute(from, to, via, hint, ports)` — from/to/via 받아서 직각 폴리라인 생성
- `resolvePipe(spec, ports)` — 파이프 spec → points 배열
- `detectCrossings(resolvedPipes)` — 모든 파이프 쌍 검사하여 직각 교차점 찾기

**buildScene()** — 모든 걸 묶는 메인 함수:
1. `resolveAnchors()` — Port-anchor 해결 (탱크→펌프→필터→...)
2. `computePorts()` — 모든 컴포넌트 포트 계산
3. PIPES.map → `resolvePipe` (1차 라우팅)
4. `resolveOnPipeAnchors(resolvedPipes)` — 파이프 위 valve/gauge/sensor 배치
5. `computePorts()` 재계산 + 파이프 재해결 (앞 단계에서 이동된 컴포넌트 반영)
6. `detectCrossings(resolvedPipes)` — 파이프 갭 위치 결정
7. SVG 출력 순서: PANELS → PIPES → TANKS → FILTERS → MODULES → PUMPS → VALVES → SENSORS → GAUGES → LEDS → TILES

#### `js/engine.js` (~450 lines)
- `state` 객체 — tanks, pumps, valves, sensors, gauges, cartridges, events, paused, flowMultiplier, currentMode
- `init()` — TOPOLOGY 배열에서 state 초기화 + 모든 visual 함수 한 번씩 호출
- `tick()` — 250ms 마다 호출:
  - 각 활성 펌프에 대해 source → dest 액체 이동 (LPM 기반)
  - 게이지 값 업데이트 (펌프 운전 상태에 따라)
  - 센서 값 잔잔한 노이즈 추가
  - 모든 visual 갱신
  - 타일 갱신 (Flow In/Out, Recovery, Alarms)
- `updateRoutes()` — 모든 .flow 클래스 비활성화 후, 활성 펌프의 pipes/valves에 .active 부여
  - **모듈 활성**: uf-system은 uf-pump-a/b 운전 시, mbr-carb-l/r는 ro-press 또는 aer-drain 운전 시
  - **MBR aeration**: air-blower 운전 시 mbr 컴포넌트에 .aerating 클래스
- 비주얼 함수:
  - `setTankVisual(id)` — 액체 높이/% 텍스트/알람 색상
  - `setPumpVisual(id)` — .running 클래스 + LED
  - `setValveVisual(id)` — .open/.closed 클래스 (핸들 회전 CSS)
  - `setSensorVisual(id)` — 값 + 알람
  - `setGaugeVisual(id)` — 바늘 회전 + 디지털 readout
- 액션:
  - `togglePump(id)`, `toggleValve(id)`, `setTankLevel(id, pct)`, `setSensor(id, val)`, `setRpm(id, rpm)`, `isolateCartridge(cid)`
  - `applyMode(mode)` — NORMAL/BACKWASH/CIP/STOP/CHEM-DOSE 프리셋
  - `triggerDemoAlarm()` — RAW HH + WF LL + pH HI 동시 트리거
  - `reset()` — 초기 상태 복구

#### `js/ui.js` (~240 lines)
- `wireSceneClicks()` — SVG에 단일 클릭 핸들러:
  - 카트리지 클릭 → isolateCartridge
  - 일반 컴포넌트 클릭 → select() + 펌프/밸브면 토글
- `renderInspector()` — 선택된 컴포넌트의 상세 패널 (탱크는 슬라이더, 펌프는 RPM, 센서는 값 입력 등)
- `wireSidebarButtons()` — 모드 버튼 (NORMAL/BACKWASH/CIP/STOP/+CHEM-DOSE)
- `wireGlobalControls()` — Reset, Pause, Alarm, Flow Multiplier, Clear Log, Deselect
- `tickClock()` — 1초 주기 시계 갱신

#### `js/main.js` (~40 lines)
부트스트랩. DOMContentLoaded에서:
1. `INVYONE_TOPO.buildScene()` 호출 → SVG 문자열 받아서 `#scene-content`에 innerHTML
2. `INVYONE_ENGINE.init()`
3. `INVYONE_UI.init()`
4. `INVYONE_ENGINE.start()` → tick 시작
5. `window.INVYONE` 노출 (브라우저 콘솔 디버그용)

---

## 3. **Anchor 시스템** (이 프로젝트의 핵심)

### 3.1 왜 만들었나
초기엔 모든 컴포넌트가 hardcoded `x, y` 좌표를 가졌음. 결과:
- 컴포넌트 하나 옮기면 연결된 파이프 좌표 다 다시 손봐야 함
- 미세 어긋남이 누적되며 시각적 품질 저하
- 사용자 피드백: "타입별로 입구 다 했는데 왜 아직도 어긋나냐"

→ **Port-anchor + onPipe-anchor 시스템 도입**으로 해결.

### 3.2 두 가지 anchor 종류

#### A. Port-anchor (포트 기반)
```js
{ id: 'sand-out', anchor: { to: 'sand.bot', myPort: 'in', dy: 14 }, ... }
```
- `to: 'comp.port'` — 부모 컴포넌트의 어느 포트에 정렬할지
- `myPort: 'X'` — 내 어느 포트가 거기에 맞붙을지
- `dx, dy` — 추가 오프셋

해석: "내 `in` 포트가 sand의 `bot` 포트보다 14px 아래에 위치"

#### B. onPipe-anchor (파이프 위)
```js
{ id: 'v-input', anchor: { onPipe: 'p-bwa1-pf', at: 0.30 }, ... }
```
- `onPipe: 'pipe-id'` — 어느 파이프 위에 놓을지
- `at: 0..1` — 파이프 길이의 fraction
- `myPort: 'center'` (기본) — 내 어느 포트가 그 지점에 정렬될지
- `dx, dy` — 추가 오프셋

해석: "p-bwa1-pf 파이프의 30% 지점에 내 center를 놓음"

### 3.3 어떻게 동작하나
1. **getPortAbsPos(comp, port)** — 컴포넌트 타입(`tank`, `ac`, `dosing`, `mbr`, `pump`, `pumpV`, `airBlower`, `valve`, `filter`, `gauge`, `sensor`, `led`, `membraneV`, `membraneH`, `carbon`)별로 포트 좌표 계산
2. **tagTypes()** — TANKS/PUMPS/VALVES/etc. 배열의 모든 컴포넌트에 `_type` 부여
3. **resolveAnchors()** — 멀티패스 (최대 12회):
   - 각 컴포넌트의 anchor 검사
   - 부모(`anchor.to`)가 이미 resolved라면 해당 포트 위치 가져와서 내 위치 계산
   - 부모가 아직 resolved 아니면 다음 패스에서 재시도
   - 의존성 그래프 자동 해결
4. **resolveOnPipeAnchors(resolvedPipes)** — 파이프 라우팅이 끝난 후, onPipe-anchor 컴포넌트들을 파이프 위에 배치

### 3.4 root component (절대 좌표 유지)
모든 anchor 시스템은 어딘가 시작점이 필요하므로 **root**는 절대 좌표 유지:
| 카테고리 | root 컴포넌트 | 이유 |
|---|---|---|
| TANKS | 11개 모두 (sand, ac, raw, chem, water-filter, chem-cep-1, chem-cep-2, cip-tank, mbr, dp, regulating) | 레이아웃 고정 위치 |
| PUMPS | bw-a1, air-blower, ro-feed | 시스템 입구/독립 컴포넌트 (자연스러운 부모 없음) |
| TILES | 4개 (FLOW IN/OUT/RECOVERY/ALARMS) | 우측 사이드바 UI |
| PANELS | 3개 (cep, cip, mbr-frame) | 외곽 데코레이션 |

총 **21개 root**. 나머지 **47개 컴포넌트**는 모두 anchor로 자동 위치 계산.

### 3.5 좌표 사용 통계 (현재)

| 카테고리 | 총 | hardcoded | anchor |
|---|---|---|---|
| 탱크 | 11 | 11 (root) | 0 |
| 펌프 | 13 | 3 (root) | **10** |
| 밸브 | 12 | 0 | **12 (전부 onPipe)** |
| 필터 | 4 | 0 | **4** |
| 모듈 (UF/RO/Carbon) | 4 | 0 | **4** |
| 게이지 | 11 | 0 | **11** |
| 센서 | 5 | 0 | **5** |
| LED | 2 | 0 | **1** + 1 root |
| 타일 | 4 | 4 (UI) | 0 |
| 패널 | 3 | 3 (deco) | 0 |
| **합계** | **69** | **21** | **47** |

### 3.6 사용 예시 (실제 topology 코드)
```js
// 탱크는 root
{ id: 'sand', x: 340, y: 120, w: 96, h: 150, type: 'ac', ... },

// 펌프는 탱크에 anchor → 탱크 옮기면 펌프 자동 따라감
{ id: 'sand-out', anchor: { to: 'sand.bot', myPort: 'in', dy: 14 }, orient: 'v', ... },

// 다음 펌프는 이전 펌프에 anchor (체인)
{ id: 'uf-pump-a', anchor: { to: 'bw-a2.out', myPort: 'in', dx: 120 }, ... },
{ id: 'uf-pump-b', anchor: { to: 'uf-pump-a.bot', myPort: 'top', dy: 12 }, ... },

// 필터는 펌프 토출에 anchor
{ id: 'pump-filter', anchor: { to: 'bw-a1.out', myPort: 'left', dx: 107 }, ... },

// 밸브는 파이프 위에 anchor → 파이프 라우팅 바뀌면 밸브 자동 이동
{ id: 'v-input', anchor: { onPipe: 'p-bwa1-pf', at: 0.30 }, ... },
{ id: 'v-uf-in', anchor: { onPipe: 'p-bw2-uf', at: 0.75 }, ... },

// 게이지는 펌프에 anchor → 펌프 옮기면 게이지 자동 따라감
{ id: 'p-in-1', anchor: { to: 'bw-a1.top', myPort: 'center', dx: -23, dy: -110 }, ... },

// 센서는 MBR에 anchor (sensor row)
{ id: 'do', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -250, dy: -80 }, ... },
{ id: 'temp', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -130, dy: -80 }, ... },

// 모듈 (UF/RO/Carbon)은 인접 컴포넌트에 anchor
{ id: 'uf-system', anchor: { to: 'uf-filter.right', myPort: 'topLeft', dx: 62, dy: -122 }, ... },
{ id: 'mbr-carb-l', anchor: { to: 'mbr.top', myPort: 'topLeft', dx: -183, dy: 106 }, ... },
```

---

## 4. 파이프 시스템

### 4.1 파이프 spec 형식
```js
// 단순 (자동 라우팅 + 직각 corner 자동 삽입)
{ id: 'p-bw2-uf', from: 'bw-a2.out', to: 'uf-pump-a.in' },

// via 웨이포인트 지정
{ id: 'p-ac-raw', from: 'ac-out.out', to: 'raw.top', via: [{ x: 605 }, { y: 90 }] },

// hint로 첫 corner 방향 제어
{ id: 'p-chem-uf', from: 'chem.bot', to: 'chem-uf.in', hint: 'v-first' },
```

**spec 필드**:
- `from`, `to`: `'comp.port'` 문자열 또는 `{x, y}` 객체
- `via`: 웨이포인트 배열. 각 항목 `{x, y}`, `{x: only}`, `{y: only}`, 또는 `'comp.port'` 문자열
  - 부분 좌표(`{x: only}`)는 직전 위치의 다른 축을 유지
- `hint`: `'h-first'` (기본) 또는 `'v-first'` — 마지막 corner 방향
- `points`: 명시적 절대 좌표 배열 (사용 시 from/to/via/hint 무시)

### 4.2 자동 corner 삽입
`autoRoute(from, to, via, hint, ports)`:
- 각 웨이포인트 사이에 직각만 사용
- 직전과 다음이 둘 다 다른 축이면 implicit corner 추가
- 마지막 to까지의 corner는 hint로 결정

예: `from: (100, 50)`, `via: [{x: 200, y: 100}]`, `to: (300, 200)`
- (100,50) → corner (100,100) → (200,100) → corner (200, ...). hint h-first → corner (300, 100) → (300, 200)
- 최종: [(100,50), (100,100), (200,100), (300,100), (300,200)]

### 4.3 파이프 교차 자동 감지 (P&ID 표준 점프)
`detectCrossings(resolvedPipes)`:
- 모든 파이프 쌍에 대해 모든 segment 쌍 검사
- 한쪽이 수직, 다른쪽이 수평이고 정확히 한 점에서 교차하면 crossing 등록
- **세로 파이프가 가로 파이프 위로 점프** (vertical gets gap)

`COMP.pipe`의 갭 처리:
- `gaps: [{x, y}, ...]` 배열 받음
- 해당 위치에서 18px (GH × 2) 갭 생성
- 갭 위에 곡선 점프 아치 (메탈 그라디언트) 그림
- flow line은 path with 반원 호 (A 명령)로 갭 위를 우회

현재 검출되는 교차:
- `p-chem-uf` (세로) ↔ `p-bw2-uf` (가로) — 챔 주입 라인이 BW-B→UF 라인 위로 점프

---

## 5. 시각 정교화 항목 (정교한 디테일들)

### 5.1 컴포넌트 디테일
| 컴포넌트 | 디테일 |
|---|---|
| **큰 탱크** | 측면에 0/25/50/75/100 메이저 틱, 25/75 미디엄 틱, 10/20/30/40/60/70/80/90 마이너 틱 |
| **AC/SAND 사일로** | 콘 상단 (22px) + 솔리드 스커트 베이스 (8px) + 하단 6px outlet stub |
| **가로 펌프** | 모터에 4개 쿨링 핀 + 펌프 하단 흐름 방향 화살표 |
| **세로 펌프 (pumpV)** | 상단 인입 + 모터(쿨링핀 5개) + 중앙 임펠러 + 우측 토출 + 하단 LED |
| **에어 블로어** | 4개 곡선 잎사귀 블레이드 (부드러운 fan 모양), 모터 본체 안 작은 LED |
| **밸브** | 자체 파이프 rect 제거 → 본체 파이프 흐름 끊기지 않음. 핸들 90° 회전 표시 |
| **압력계** | 0~max 다이얼, 정상(녹)/주의(주황)/알람(분홍) 3구간 호, 회전 바늘 + 디지털 readout |
| **UF SYSTEM** | 5개 세로 술병 (적색 캡 상하), 활성 시 fluidPulse 애니메이션 |
| **RO SYSTEM** | 5개 가로 원통 적층, 활성 시 fluidPulse |
| **MBR 카본** | 5개 세로 카트리지 × 2뱅크, 배경 투명 (MBR 액체 비침), 활성 시 펄스 |
| **MBR** | 외곽 청록 발광 (aerating 시), 7개 버블 상승 + 페이드 |
| **필터** | 단순 흰색 통, 내부 % 텍스트 없음 |
| **파이프** | rect 기반 직각 덕트 (메탈 + 어두운 채널) + flow line 애니 |
| **파이프 교차** | 세로 파이프 갭 + 곡선 점프 아치 (P&ID 표준) |

### 5.2 활성 상태 표시
| 상태 | 시각 |
|---|---|
| 펌프 ON | 임펠러 회전 (0.8s linear) + LED 녹색 |
| 펌프 OFF | 정지 + LED 어둠 |
| 밸브 OPEN | 핸들 가로 (기본) + 본체 어두운 녹색 |
| 밸브 CLOSED | 핸들 90° 회전 (세로) + 본체 어둠 |
| 흐름 활성 | dash 행진 애니 (0.6s linear) |
| 흐름 차단 | 빨간 깜빡임 |
| 탱크 알람 HH/LL | 레벨박스 빨강 펄스 |
| 탱크 알람 HL/LH | 레벨박스 주황 펄스 |
| 모듈 활성 | cell-fluid 펄스 (opacity + scaleY) + 매니폴드 라인 굵게 + 청록 발광 |
| MBR 폭기 중 | 외곽 청록 발광 + 7개 버블 상승 |

---

## 6. 엔진 (시뮬레이션) 모델

### 6.1 흐름 모델
- 매 250ms tick:
  - 각 펌프에 대해 `pumpRouteActive(p)` 검사
  - 활성 조건: 펌프 ON + 모든 valves OPEN + source 탱크 잔량 > 0 + dest 탱크 여유 > 0
  - 활성 시: `LPM × (RPM/1450) × flowMultiplier × (250/60000)` 리터를 source에서 차감 / dest에 적립

### 6.2 압력 모델 (간단 휴리스틱)
- 각 게이지는 `source` 펌프와 연결
- 펌프 ON + 경로 active → 게이지 값 약 55% × max
- 펌프 ON + 경로 blocked → 약 85% × max (압력 급상승)
- 펌프 OFF → 0
- smoothing: `value = value * 0.85 + target * 0.15`

### 6.3 알람 임계
| 항목 | 알람 |
|---|---|
| 탱크 수위 | HH (≥95%) / HL (≥85%) / LH (≤5%) / LL (≤20%) |
| 센서 (DO/Temp/TSS/pH) | 각 센서별 lo/hi 임계 (예: pH 6.5~8.5 정상) |
| 압력계 | 75% 이상 빨간 배경 |

### 6.4 운전 모드 프리셋
| 모드 | 동작 |
|---|---|
| **NORMAL** | 모든 메인 밸브 OPEN, 모든 메인 펌프 ON (uf-pump-b, cip-pump, chem-uf 제외) |
| **BACKWASH** | UF 라인만 ON (bw-a1, bw-a2 + V-IN, V-UF-IN, V-UF-OUT) |
| **CIP** | CIP 라인 ON (cip-pump + v-cip-out, v-ro-in) |
| **+CHEM-DOSE** | chem-uf 펌프 + v-chem-uf ON (UF에 케미컬 주입) |
| **STOP** | 모두 OFF |

---

## 7. 현재 컴포넌트 카운트 (검증된 수치)

| 카테고리 | 개수 |
|---|---|
| 탱크 | 11 (sand, ac, raw, chem, water-filter, chem-cep-1, chem-cep-2, cip-tank, mbr, dp, regulating) |
| 펌프 | 13 (bw-a1, sand-out, ac-out, bw-a2, chem-uf, uf-pump-a, uf-pump-b, ro-feed, ro-press, cip-pump, aer-drain, dp-out, air-blower) |
| 밸브 | 12 (v-input, v-sand-out, v-ac-out, v-raw-out, v-chem-uf, v-uf-in, v-uf-out, v-ro-in, v-ro-press-out, v-cip-out, v-mbr-out, v-dp-out) |
| 파이프 | 26 |
| 필터 | 4 (pump-filter, uf-filter, ro-filter, cip-filter) |
| 모듈 | 4 (uf-system, ro-system, mbr-carb-l, mbr-carb-r) |
| 압력계 | 11 |
| 센서 | 5 (do, temp, tss, ph, tss-raw) |
| LED | 2 |
| 타일 | 4 (FLOW IN/OUT, RECOVERY, ACTIVE ALARMS) |
| 패널 | 3 (CEP, CIP, mbr-frame) |
| **총** | **95** |

---

## 8. 개발 여정 (반복 학습)

### 8.1 Stage 1 — 좌표 기반 단순 배치 (실패)
- 모든 컴포넌트 hardcoded x/y
- 결과: 매번 어긋나고 사진과 안 맞음
- 사용자 피드백: "엉망징창"

### 8.2 Stage 2 — 그리드 + 직각 파이프 (개선)
- Codex와 협업으로 그리드 시스템 도입 (gx(n) = 40 + n*160)
- 파이프를 stroke path → rect 세그먼트로 변경
- 직각만 허용
- 18개 파이프 모두 직각 라우팅
- 결과: 향상되었지만 아직 어긋남

### 8.3 Stage 3 — 포트 기반 파이프 (개선)
- 모든 파이프를 `'comp.port'` 형식으로
- `autoRoute()` 함수로 직각 폴리라인 자동 생성
- `via` 웨이포인트 지원
- 결과: 파이프 라우팅 자체는 깔끔. 근데 컴포넌트 위치는 여전히 hardcoded

### 8.4 Stage 4 — UF 세로 / RO 가로 분기 (시각 정확성)
- `membraneBank`에 `orientation` 파라미터 추가
- UF=vertical (5 세로 술병), RO=horizontal (5 가로 원통)
- 카본 뱅크 배경 투명화 (MBR 액체 비침)

### 8.5 Stage 5 — 정교화 (P&ID 표준)
- 파이프 자동 교차 감지 + 점프 아치
- 탱크 측면 스케일 눈금
- 펌프 모터 쿨링 핀 + 흐름 방향 화살표
- MBR 폭기 버블 애니메이션
- AIR BLOWER 4개 곡선 잎사귀 블레이드
- 밸브 자체 파이프 rect 제거 (본체 파이프 흐름 통과)
- UF/MBR 활성 펄스 애니메이션
- 세로 펌프 (pumpV) 컴포넌트 추가

### 8.6 Stage 6 — Anchor 시스템 (현재)
- Port-anchor 도입 (`{ to: 'comp.port', myPort: 'X', dx, dy }`)
- onPipe-anchor 도입 (`{ onPipe: 'pipe-id', at: 0.5 }`)
- 47개 컴포넌트가 anchor로 자동 위치 계산
- 21개 root만 hardcoded

---

## 9. 알려진 이슈 / 제한사항

### 9.1 시각적 잔여 이슈
| 이슈 | 심각도 | 원인 | 해결책 |
|---|---|---|---|
| `p-wf-ro` 긴 가로 파이프 (1450px) | 중 | 구조적 (WF 우상, RO FEED 좌하) | CEP 위치 재배치 또는 캔버스 폭 축소 |
| 일부 밸브가 탱크 벽에 살짝 겹침 | 약 | 의도된 P&ID 스타일 (탱크 outlet에 valve) | 그대로 둠 |
| AERATION DRAIN이 MBR 우측 영역에 살짝 겹침 | 약 | 의도 (펌프가 탱크 outlet에 위치) | 그대로 둠 |
| `p-raw-bw2` 파이프 짧아서 v-raw-out 약간 겹침 (4px) | 약 | 구조적 (RAW 우측에 BW-B 너무 가까움) | BW-B 더 우측으로 이동 또는 valve 더 작게 |

### 9.2 미구현/미완성
- 실제 PLC/MQTT/OPC UA 연결 없음 (mock state)
- 안전 인터록·권한 분리·이력관리(historian)·알람 ack 미구현
- 압력 모델은 단순 휴리스틱 (실제 펌프 곡선·배관 마찰 무시)
- 단일 페이지 (실 입찰 스코프는 폐수3 + 오수2 + 약품탱크실8 + 대기29기로 화면 분할 필요)
- 한국어 일부 (i18n 미구현, 한·영·베트남어 토글 가치 있음)
- 알람 ack 워크플로우 없음
- 인증/권한 게이트 없음 (입찰 조건엔 알람 OFF 패스워드 필요)

### 9.3 알려진 예외 케이스
- 컴포넌트가 anchor를 따라 계산할 때 CSS transform-origin이 SVG에서 일관되지 않을 수 있음 — 현재 `transform-box: fill-box; transform-origin: center;` 사용
- `setGaugeVisual`은 SVG `transform="rotate(angle 40 40)"` 어트리뷰트 직접 설정 (CSS rotation 불안정해서)
- `pipePointAt`은 Manhattan 길이 사용 (직각 파이프 가정)

---

## 10. 콘솔 디버그 API (브라우저 DevTools)

페이지 로드 후 브라우저 콘솔에서:

```js
// 도움말
INVYONE.help()

// 상태 객체 직접 접근
INVYONE.engine.state              // 전체 state
INVYONE.engine.state.tanks        // 모든 탱크
INVYONE.engine.state.pumps        // 모든 펌프

// 액션
INVYONE.engine.applyMode('NORMAL')        // 모드 변경
INVYONE.engine.togglePump('uf-pump-a')    // 펌프 토글
INVYONE.engine.toggleValve('v-uf-in')     // 밸브 토글
INVYONE.engine.setTankLevel('raw', 90)    // 탱크 수위
INVYONE.engine.setSensor('ph', 9.5)       // 센서 값
INVYONE.engine.setRpm('uf-pump-a', 2000)  // 펌프 RPM
INVYONE.engine.triggerDemoAlarm()         // 데모 알람
INVYONE.engine.reset()                    // 초기화

// 토폴로지 검사
INVYONE.topo.computePorts()                       // 모든 포트 좌표
INVYONE.topo.resolvePipe(spec, ports)             // 파이프 해결

// 강제 시뮬레이션 일시정지
INVYONE.engine.state.paused = true

// 유량 배율
INVYONE.engine.state.flowMultiplier = 20
```

---

## 11. 내일 이어서 할 작업 (TODO 우선순위)

### 11.1 Critical (PT 전 반드시)
- [ ] 전체 페이지 레이아웃 시각 점검 (브라우저에서 한 번 더)
- [ ] 모드 전환 동작 검증 (NORMAL→BACKWASH→CIP→STOP 시 흐름 변화 자연스러운지)
- [ ] 알람 데모 동작 검증 (트리거 시 즉각 반응)
- [ ] PT용 시연 시나리오 정리 (어떤 순서로 클릭/조작할지 스크립트)

### 11.2 High (가능하면)
- [ ] 알람 ack 워크플로우 (알람 발생 → 사이드패널 ACK 버튼 → 해소 로그)
- [ ] 알람 OFF 시 패스워드 게이트 (입찰 조건)
- [ ] 시연 시나리오 자동 재생 모드 (옵션)
- [ ] 트렌드 그래프 (탱크 수위/압력 시계열, mini sparkline)

### 11.3 Nice to have
- [ ] i18n 토글 (한국어/영어/베트남어)
- [ ] 다화면 라우터 (메인 + 약품탱크실 + 대기방지 시설)
- [ ] PDF 보고서 생성 (이벤트 로그 → PDF)
- [ ] PT 모드 (큰 글씨, 전체화면 자동)

### 11.4 잠재 리팩터링
- [ ] root 컴포넌트도 grid 시스템 사용 (gx()/gy() 함수)
- [ ] panels/tiles도 anchor 시스템에 포함
- [ ] CSS animation 성능 최적화 (will-change, transform-only)

---

## 12. 빠르게 페이지 열기

1. 파일 탐색기에서 `C:\Users\defaultuser0\Downloads\invyone-stage2-demo-v1\index.html` 더블클릭
2. 또는 브라우저에 `file:///C:/Users/defaultuser0/Downloads/invyone-stage2-demo-v1/index.html`
3. 외부 의존성/빌드 단계 없음 — 바로 동작

> 캐시 문제 시: `Ctrl + Shift + R` 강제 새로고침

---

## 13. 핵심 요약 (한 줄씩)

- ✅ **포트 기반 파이프**: 모든 파이프가 `'comp.port'` 참조로 자동 라우팅
- ✅ **Anchor 시스템**: 47개 컴포넌트가 자동 위치 계산, 21개 root만 hardcoded
- ✅ **자동 직각 파이프**: `autoRoute(from, to, via, hint)` 함수로 직각 폴리라인 자동 생성
- ✅ **자동 교차 감지 + P&ID 점프**: 파이프 교차 시 자동으로 갭 + 곡선 점프 아치
- ✅ **세로 펌프 분기**: SAND/AC/CHEM 펌프가 탱크 바로 아래 정렬되는 vertical orientation
- ✅ **UF/RO 분기**: UF=세로 술병, RO=가로 원통 (사진 일치)
- ✅ **MBR 폭기 시각화**: AIR BLOWER ON 시 외곽 발광 + 버블 상승
- ✅ **모듈 활성 펄스**: UF/RO/MBR-카본 운전 시 fluid pulse 애니메이션
- ✅ **탱크 측면 스케일**: 0/25/50/75/100 눈금
- ✅ **펌프 디테일**: 모터 쿨링 핀 + 흐름 방향 화살표
- ✅ **밸브 무파이프**: 본체 파이프 흐름이 밸브 영역에서 끊기지 않게
- ✅ **5단계 모드**: NORMAL/BACKWASH/CIP/STOP/+CHEM-DOSE
- ✅ **알람 시스템**: 탱크 HH/HL/LH/LL + 센서 lo/hi + 압력 75%↑
- ✅ **이벤트 로그**: 시간순 + 레벨별 색상
- ✅ **인스펙터**: 클릭한 컴포넌트의 상세 컨트롤 (슬라이더 등)
- ✅ **콘솔 API**: `INVYONE.engine.*` 으로 직접 제어 가능

PT까지 8일 남음. 핵심 기능은 완성. 내일은 시각 미세조정 + 시연 시나리오 정리.
