docs(screener): node canvas mode design spec
n8n 스타일 노드 캔버스 모드 설계 문서. 폼 모드와 토글로 전환, 같은 settings state 공유. 11 노드 + 16 엣지 고정 토폴로지, react-flow 기반 시각화. 백엔드 변경 없음. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
505
docs/superpowers/specs/2026-05-13-screener-node-canvas-design.md
Normal file
505
docs/superpowers/specs/2026-05-13-screener-node-canvas-design.md
Normal file
@@ -0,0 +1,505 @@
|
||||
# Stock Screener — Node Canvas Mode Design
|
||||
|
||||
**작성일**: 2026-05-13
|
||||
**작성자**: gahusb
|
||||
**상태**: Approved for implementation
|
||||
**선행 spec**: `2026-05-12-stock-screener-board-design.md` (§14 — react-flow 노드 캔버스 후속 슬라이스)
|
||||
|
||||
---
|
||||
|
||||
## 1. 목표
|
||||
|
||||
`/stock/screener` 페이지에 **n8n 스타일 노드 캔버스 모드**를 추가한다. 폼 모드와 토글로 전환하며, 같은 settings state를 공유한다. 백엔드는 변경하지 않는다 — 캔버스는 시각화 + 편집 UI일 뿐, 결과적으로는 동일한 `weights / node_params / gate_params` 를 `/api/stock/screener/run` 에 전송한다.
|
||||
|
||||
**Why**: 사용자가 슬라이더만 들여다보는 폼 모드는 "어떤 노드가 어떤 단계에서 무엇을 하는지"의 파이프라인 감각이 약하다. n8n/Figma류 캔버스 시각화는 데이터 흐름을 한눈에 보여줘 강세주 분석 모델의 구조적 이해를 돕는다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 범위
|
||||
|
||||
**포함 (이번 슬라이스)**:
|
||||
- 헤더 토글 (`폼 ↔ 캔버스`) — 데스크탑 전용
|
||||
- 11개 노드의 미니 파이프라인 시각화 (고정 토폴로지)
|
||||
- 점수 노드 카드 위 가중치/활성/핵심 파라미터 인라인 편집 + 설명 표시
|
||||
- floating 미니 툴바 (실행 / 저장 실행 / 설정 영구 저장 / 레이아웃 리셋)
|
||||
- 노드 위치 localStorage 저장 + 초기화 버튼
|
||||
- 모바일에서는 캔버스 토글 숨김, 폼 강제
|
||||
|
||||
**범위 외 (NOT)**:
|
||||
- 노드 추가/삭제 UI (토폴로지 고정)
|
||||
- 노드 간 연결선 사용자 편집
|
||||
- 자유 그래프 모드 (별도 후속 슬라이스)
|
||||
- 캔버스 안 결과 노드에 결과 표시 (외부 테이블에만 표시)
|
||||
- 노드 캔버스 화면 자체에서의 대화형 백테스트
|
||||
- dagre 등 자동 레이아웃 알고리즘
|
||||
|
||||
---
|
||||
|
||||
## 3. 아키텍처 개요
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ Screener.jsx (entrypoint) │
|
||||
│ - useScreenerMode (form|canvas) │
|
||||
│ - useIsMobile() → 강제 form │
|
||||
└────────────┬────────────────┘
|
||||
│
|
||||
┌────────────────┼────────────────┐
|
||||
│ │ │
|
||||
form mode canvas mode shared result area
|
||||
(기존 그대로) (신규) (기존 그대로)
|
||||
│ │ │
|
||||
┌──────────┴──┐ ┌─────────┴──────┐ ┌────┴──────┐
|
||||
│ GatePanel │ │ ScreenerCanvas │ │ ResultTable
|
||||
│ NodePanel │ │ + CanvasToolbar│ │ TelegramPreview
|
||||
│ GlobalControls│ │ + Node cards │ │ RunHistoryList
|
||||
└──────────────┘ └─────────────────┘ └───────────┘
|
||||
↑ ↑ ↑
|
||||
└────────────────┴────────────────┘
|
||||
공유 state: useScreenerSettings,
|
||||
useScreenerRun, useScreenerHistory
|
||||
```
|
||||
|
||||
**의존성 추가**: `@xyflow/react` (구 react-flow, MIT, ~50KB gzipped).
|
||||
|
||||
**백엔드 변경 없음**. 캔버스는 settings를 동일한 형태로 만들고, 동일한 `/run` 엔드포인트를 호출한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 화면 레이아웃
|
||||
|
||||
### 4.1 데스크탑 — 캔버스 모드
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────┐
|
||||
│ Header: 스크리너 [폼] [캔버스] │
|
||||
│ 최근 자동잡: 2026-05-13 · 분석 기준일: 2026-05-13│
|
||||
├───────────────────────────────────────────────────────────┤
|
||||
│ ╔═════════════════════════════════════════════════════╗ │
|
||||
│ ║ ┌─ floating toolbar ──────────────────────────┐ ║ │
|
||||
│ ║ │ ▶ 실행 💾 저장 실행 📌 설정 저장 🔄 ⛶ │ ║ │
|
||||
│ ║ └──────────────────────────────────────────────┘ ║ │
|
||||
│ ║ ║ │
|
||||
│ ║ ┌─────┐ ┌──────┐ ┌───────┐ ║ │
|
||||
│ ║ │📥KRX│→ │🛡️위생│ ┬→│외국인 │ ┐ ║ │
|
||||
│ ║ │data │ │gate │ ├→│거래량 │ │ ┌─────────────┐ ║ │
|
||||
│ ║ └─────┘ └──────┘ ├→│모멘텀 │ ┼→ │⚙️가중합+TopN │→ │📊│║│
|
||||
│ ║ ├→│52w고가│ │ │ +ATR 사이저 │ ║ │
|
||||
│ ║ ├→│RS │ │ └─────────────┘ ║ │
|
||||
│ ║ ├→│이평선│ ┤ ║ │
|
||||
│ ║ └→│VCP │ ┘ ║ │
|
||||
│ ║ ║ │
|
||||
│ ║ (캔버스 영역: 화면 높이의 약 60-65%) ║ │
|
||||
│ ╚═══════════════════════════════════════════════════════╝ │
|
||||
├───────────────────────────────────────────────────────────┤
|
||||
│ ResultTable (기존 그대로) — 비교 모드 그대로 │
|
||||
│ TelegramPreview (기존 그대로) │
|
||||
│ RunHistoryList (기존 그대로 — 우측 사이드) │
|
||||
└───────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**그리드 구성 (캔버스 모드)**:
|
||||
|
||||
- Row 1 — 헤더 (높이 자동)
|
||||
- Row 2 — 캔버스 영역 (`min-height: 60vh`, `max-height: 70vh`)
|
||||
- Row 3 — 2-column: 좌측 `ResultTable + TelegramPreview` (flex 1), 우측 `RunHistoryList` (width 300px)
|
||||
|
||||
폼 모드의 3-column 그리드(좌 사이드/센터/우 사이드)와 달리, 캔버스 모드는 캔버스가 가로 전체를 쓰고 결과 영역만 2-column으로 분리. `RunHistoryList` 의 위치는 두 모드 모두 "우측 결과 사이드"로 일관.
|
||||
|
||||
### 4.2 데스크탑 — 폼 모드
|
||||
|
||||
기존 layout 그대로. 헤더에 토글 [폼] [캔버스]만 추가.
|
||||
|
||||
### 4.3 모바일 (<768px)
|
||||
|
||||
기존 모바일 카드 layout 그대로. 헤더 토글 자체를 렌더하지 않음. localStorage에 `mode='canvas'`로 저장돼 있어도 무시.
|
||||
|
||||
---
|
||||
|
||||
## 5. 노드 종류
|
||||
|
||||
총 11개 노드, 4개 카테고리.
|
||||
|
||||
| 카테고리 | 노드 | 편집 | 색상 | 표시 정보 |
|
||||
|----------|------|------|------|-----------|
|
||||
| **데이터** | `📥 KRX 데이터` | 불가 | 회색 | "~2,800종목 · FDR" |
|
||||
| **게이트** | `🛡️ 위생 게이트` | 가능 | 노랑 | 파라미터 (min_market_cap 등) + 활성/비활성 |
|
||||
| **점수** | `📈 외국인` | 가능 | 컬러 | 가중치 + 핵심 파라미터 + 설명 |
|
||||
| **점수** | `📊 거래량 급증` | 가능 | 컬러 | 동일 |
|
||||
| **점수** | `🚀 모멘텀` | 가능 | 컬러 | 동일 |
|
||||
| **점수** | `🔝 52w 고가` | 가능 | 컬러 | 동일 |
|
||||
| **점수** | `💪 RS Rating` | 가능 | 컬러 | 동일 |
|
||||
| **점수** | `📉 이평선 정렬` | 가능 | 컬러 | 동일 |
|
||||
| **점수** | `🌀 VCP-lite` | 가능 | 컬러 | 동일 |
|
||||
| **결합** | `⚙️ 가중합+TopN+ATR` | 불가 | 회색 | "TopN=10 · ATR×2" 등 현재 settings 요약 |
|
||||
| **결과** | `📊 결과` | 불가 | 회색 | "마지막 실행: 2026-05-13 · 8종목 통과" |
|
||||
|
||||
점수 노드의 컬러는 기존 `NODE_META` 의 accent color 시스템과 동기화 — 폼 모드에서 쓰던 색상이 캔버스에서도 동일하게 적용.
|
||||
|
||||
---
|
||||
|
||||
## 6. 노드 카드 디자인
|
||||
|
||||
### 6.1 점수 노드 카드 (편집 가능)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ 📈 거래량 급증 ⓘ │ ← 호버 시 풀 설명 툴팁
|
||||
│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │
|
||||
│ "20일 평균 대비 2배 이상" │ ← 항상 표시되는 한 줄 요약
|
||||
│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │
|
||||
│ 가중치 [█████░░░░░] 0.5 │ ← 슬라이더 (0~1, step 0.05)
|
||||
│ ☑ 활성 │ ← 체크박스. uncheck = weight 0
|
||||
│ │
|
||||
│ ▾ 파라미터 (펼치면) │
|
||||
│ lookback_days: [ 20 ] 일 │
|
||||
│ multiplier: [2.0 ] │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 한 줄 요약: 기존 `NODE_META[name].summary` (없으면 `description` 첫 줄)
|
||||
- 풀 설명 (호버 툴팁): 기존 `NODE_META[name].description`
|
||||
- 파라미터 폼: `param_schema` 기반 자동 생성 (기존 `NodeCard.jsx` 와 동일 로직 재사용)
|
||||
|
||||
### 6.2 게이트 노드 카드 (편집 가능, 노랑)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ 🛡️ 위생 게이트 ⓘ │
|
||||
│ "통과해야 점수 단계 진입" │
|
||||
│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │
|
||||
│ ☑ 활성 │
|
||||
│ ▾ 파라미터 │
|
||||
│ min_market_cap: [50] 억원 │
|
||||
│ exclude_spac: ☑ │
|
||||
│ ... │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.3 고정 노드 카드 (정보 표시만, 회색)
|
||||
|
||||
```
|
||||
┌────────────────────┐
|
||||
│ 📥 KRX 데이터 │
|
||||
│ ~2,800종목 · FDR │
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
결합 노드는 동적으로 현재 settings를 요약 표시:
|
||||
```
|
||||
┌────────────────────────────┐
|
||||
│ ⚙️ 가중합 + TopN + ATR │
|
||||
│ Top 10 · RR 2.0 · ATR×2 │ ← settings에서 계산해서 표시
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
결과 노드도 동적:
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ 📊 결과 │
|
||||
│ 마지막 실행: 14:32 │
|
||||
│ 8 / 12 종목 통과 │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 캔버스 인터랙션
|
||||
|
||||
| 동작 | 결과 |
|
||||
|------|------|
|
||||
| 노드 드래그 | 위치 변경 → 드래그 종료 시 `screener-canvas-layout-v1` localStorage에 저장 |
|
||||
| 슬라이더 변경 | `useScreenerSettings.setLocal({...settings, weights: {...}})` → `dirty=true` |
|
||||
| 체크박스 (활성) | weight 토글: uncheck 시 weight=0 저장, check 시 이전 값 복원 (default = 0.5) |
|
||||
| 파라미터 ▾ 펼치기 | 카드 높이 동적 확장 |
|
||||
| 마우스 휠 | 줌 (React Flow 기본) |
|
||||
| 드래그 (빈 공간) | 팬 (React Flow 기본) |
|
||||
| ⛶ fitView 버튼 | 전체 노드 화면 맞춤 |
|
||||
| 🔄 레이아웃 리셋 | `INITIAL_NODE_POSITIONS` 로 복귀, localStorage 키 삭제 |
|
||||
| ▶ 실행 | 기존 `runPreview(settings)` → 결과는 하단 ResultTable |
|
||||
| 💾 저장 실행 | 기존 `runSave(settings)` → DB 영구화 |
|
||||
| 📌 설정 저장 | 기존 `save()` (settings 영구화) |
|
||||
|
||||
엣지 연결선은 사용자가 편집할 수 없음 (고정). React Flow 인스턴스 prop `nodesConnectable={false}`, `edgesUpdatable={false}`.
|
||||
|
||||
---
|
||||
|
||||
## 8. 컴포넌트 분해 (신규 파일)
|
||||
|
||||
```
|
||||
src/pages/stock/screener/
|
||||
Screener.jsx ← 모드 토글 추가, canvas 모드 분기 렌더
|
||||
hooks/
|
||||
useScreenerMode.js ← 신규: 'form' | 'canvas' state + localStorage
|
||||
useCanvasLayout.js ← 신규: 노드 위치 read/write/reset
|
||||
(기존 hooks 그대로)
|
||||
components/
|
||||
ModeToggle.jsx ← 신규: [폼][캔버스] 세그먼트 컨트롤 (헤더용)
|
||||
canvas/
|
||||
CanvasLayout.jsx ← 신규: 캔버스 + 결과 영역 그리드 (4.1 그리드 구성)
|
||||
ScreenerCanvas.jsx ← React Flow 루트 컨테이너
|
||||
CanvasToolbar.jsx ← floating Panel (실행/저장/리셋/fitView)
|
||||
nodes/
|
||||
ScoreNodeCard.jsx ← 점수 노드 카드 (편집)
|
||||
GateNodeCard.jsx ← 게이트 노드 카드 (편집)
|
||||
FixedNodeCard.jsx ← 데이터/결합/결과 카드 (정보만)
|
||||
constants/
|
||||
canvasLayout.js ← INITIAL_NODE_POSITIONS / EDGES / NODE_KIND_MAP
|
||||
(기존 components 그대로 — 폼 모드에서 계속 사용)
|
||||
```
|
||||
|
||||
기존 컴포넌트(`GatePanel`, `NodePanel`, `GlobalControls`, `ResultTable`, `TelegramPreview`, `RunHistoryList`)는 **변경 없음**. 결과 영역은 모드와 무관하게 동일.
|
||||
|
||||
### 8.1 `Screener.jsx` 변경점
|
||||
|
||||
```jsx
|
||||
const { mode, setMode } = useScreenerMode();
|
||||
const isMobile = useIsMobile();
|
||||
const effectiveMode = isMobile ? 'form' : mode;
|
||||
|
||||
return (
|
||||
<div className="screener-page">
|
||||
<header className="screener-header">
|
||||
<h1>스크리너</h1>
|
||||
{!isMobile && (
|
||||
<ModeToggle value={mode} onChange={setMode} />
|
||||
)}
|
||||
</header>
|
||||
|
||||
{effectiveMode === 'form' ? (
|
||||
<FormLayout {...sharedProps} /> /* 기존 grid layout */
|
||||
) : (
|
||||
<CanvasLayout {...sharedProps} /> /* 신규 — 캔버스 + 동일 결과 영역 */
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 데이터 / state 설계
|
||||
|
||||
### 9.1 localStorage 키
|
||||
|
||||
| 키 | shape | 설명 |
|
||||
|----|-------|------|
|
||||
| `screener-mode-v1` | `'form' \| 'canvas'` | 마지막 사용 모드 |
|
||||
| `screener-canvas-layout-v1` | `{ [nodeId: string]: { x: number, y: number } }` | 노드별 좌표 |
|
||||
|
||||
### 9.2 `useScreenerMode`
|
||||
|
||||
```js
|
||||
export function useScreenerMode() {
|
||||
const [mode, setModeState] = useState(() => {
|
||||
try {
|
||||
return localStorage.getItem('screener-mode-v1') || 'form';
|
||||
} catch { return 'form'; }
|
||||
});
|
||||
const setMode = (m) => {
|
||||
setModeState(m);
|
||||
try { localStorage.setItem('screener-mode-v1', m); } catch {}
|
||||
};
|
||||
return { mode, setMode };
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 `useCanvasLayout`
|
||||
|
||||
```js
|
||||
export function useCanvasLayout(initialPositions) {
|
||||
const STORAGE_KEY = 'screener-canvas-layout-v1';
|
||||
const [positions, setPositions] = useState(() => readOrInit(initialPositions));
|
||||
|
||||
const updateNodePosition = (nodeId, pos) => {
|
||||
setPositions((prev) => {
|
||||
const next = { ...prev, [nodeId]: pos };
|
||||
writeSafe(next);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const reset = () => {
|
||||
setPositions(initialPositions);
|
||||
try { localStorage.removeItem(STORAGE_KEY); } catch {}
|
||||
};
|
||||
return { positions, updateNodePosition, reset };
|
||||
}
|
||||
```
|
||||
|
||||
`readOrInit` 은 JSON.parse 실패하거나 노드 ID가 누락된 경우 누락된 ID에 대해서만 `initialPositions` 값을 보충.
|
||||
|
||||
### 9.4 `canvasLayout.js` 상수
|
||||
|
||||
```js
|
||||
export const NODE_IDS = {
|
||||
DATA: 'data',
|
||||
GATE: 'gate-hygiene',
|
||||
FOREIGN: 'score-foreign-buy',
|
||||
VOLUME: 'score-volume-surge',
|
||||
MOMENTUM: 'score-momentum',
|
||||
HIGH52W: 'score-high52w',
|
||||
RS: 'score-rs-rating',
|
||||
MA: 'score-ma-alignment',
|
||||
VCP: 'score-vcp-lite',
|
||||
COMBINE: 'combine',
|
||||
RESULT: 'result',
|
||||
};
|
||||
|
||||
export const INITIAL_NODE_POSITIONS = {
|
||||
[NODE_IDS.DATA]: { x: 40, y: 280 },
|
||||
[NODE_IDS.GATE]: { x: 240, y: 280 },
|
||||
[NODE_IDS.FOREIGN]: { x: 480, y: 0 },
|
||||
[NODE_IDS.VOLUME]: { x: 480, y: 90 },
|
||||
[NODE_IDS.MOMENTUM]: { x: 480, y: 180 },
|
||||
[NODE_IDS.HIGH52W]: { x: 480, y: 270 },
|
||||
[NODE_IDS.RS]: { x: 480, y: 360 },
|
||||
[NODE_IDS.MA]: { x: 480, y: 450 },
|
||||
[NODE_IDS.VCP]: { x: 480, y: 540 },
|
||||
[NODE_IDS.COMBINE]: { x: 800, y: 280 },
|
||||
[NODE_IDS.RESULT]: { x: 1080, y: 280 },
|
||||
};
|
||||
|
||||
export const EDGES = [
|
||||
{ id: 'e-data-gate', source: NODE_IDS.DATA, target: NODE_IDS.GATE },
|
||||
...['FOREIGN','VOLUME','MOMENTUM','HIGH52W','RS','MA','VCP'].map((k) => ({
|
||||
id: `e-gate-${k.toLowerCase()}`, source: NODE_IDS.GATE, target: NODE_IDS[k],
|
||||
})),
|
||||
...['FOREIGN','VOLUME','MOMENTUM','HIGH52W','RS','MA','VCP'].map((k) => ({
|
||||
id: `e-${k.toLowerCase()}-combine`, source: NODE_IDS[k], target: NODE_IDS.COMBINE,
|
||||
})),
|
||||
{ id: 'e-combine-result', source: NODE_IDS.COMBINE, target: NODE_IDS.RESULT },
|
||||
];
|
||||
```
|
||||
|
||||
총 엣지 수: 1(data→gate) + 7(gate→점수) + 7(점수→combine) + 1(combine→result) = **16개**.
|
||||
|
||||
---
|
||||
|
||||
## 10. 시각 디자인 디테일
|
||||
|
||||
| 요소 | 스타일 |
|
||||
|------|--------|
|
||||
| 캔버스 배경 | `bg-screener-canvas` (다크 그리드, 점선 `#1f2937`) |
|
||||
| 고정 노드 카드 | 배경 `#1f2937`, 텍스트 `#9ca3af`, 200×64 |
|
||||
| 게이트 카드 | accent `#facc15` (노랑) 좌측 4px stripe, 220×auto |
|
||||
| 점수 카드 | accent = 기존 `NODE_META[name].color`, 240×auto |
|
||||
| 비활성 점수 카드 | opacity 0.45 + grayscale 0.6 |
|
||||
| 엣지 (active) | `#fbbf24` 1.5px, 약한 그라데이션 |
|
||||
| 엣지 (해당 점수 노드 weight=0) | `#374151` 1px, 점선 |
|
||||
| 미니맵 | **사용하지 않음** (캔버스 크기가 작아 불필요) |
|
||||
| Controls (줌/리셋) | React Flow `<Controls />` 좌하단, 미니멀 |
|
||||
| floating toolbar | 좌상단, `position: absolute`, `backdrop-filter: blur(8px)`, 반투명 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 모바일/엣지 케이스
|
||||
|
||||
| 케이스 | 처리 |
|
||||
|--------|------|
|
||||
| 모바일 진입 (≤768px) | 토글 미렌더, `effectiveMode = 'form'` 강제 |
|
||||
| 데스크탑 → 모바일 리사이즈 중 | `useIsMobile` 가 자동 감지 → 폼으로 폴백 |
|
||||
| localStorage 파싱 실패 | catch + reset → 초기 위치/모드로 복귀 |
|
||||
| 노드 ID 누락 (마이그레이션) | 누락 노드만 `INITIAL_NODE_POSITIONS` 값 사용, 나머지는 저장값 유지 |
|
||||
| 노드 ID 신규 추가 (후속) | 같은 누락 처리 로직으로 자동 흡수 |
|
||||
| React Flow 초기 렌더 깜빡임 | `fitView` 초기 옵션 + `defaultViewport` 명시로 흡수 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 테스트 전략
|
||||
|
||||
캔버스는 시각화 위주라 E2E 테스트 비용이 크므로 **단위 테스트 중심**으로 간다.
|
||||
|
||||
### 12.1 단위 테스트 (web-ui)
|
||||
|
||||
| 파일 | 검증 |
|
||||
|------|------|
|
||||
| `useScreenerMode.test.js` | 초기값 'form', set 후 localStorage 반영, 손상 시 fallback |
|
||||
| `useCanvasLayout.test.js` | 초기 positions 반환, updateNodePosition 후 localStorage 반영, reset 후 storage 삭제, 손상 시 initial 반환, 누락 ID 시 initial 보충 |
|
||||
| `canvasLayout.test.js` | EDGES 정합성: 모든 점수 노드가 gate 입력과 combine 출력을 가짐, source/target ID가 NODE_IDS 안에 존재 |
|
||||
| `ScoreNodeCard.test.jsx` | 슬라이더 onChange 호출, 비활성 체크박스 시 weight=0, 활성 복원 시 default 0.5 |
|
||||
|
||||
### 12.2 통합 (가볍게)
|
||||
|
||||
- `Screener.test.jsx` 회귀: 폼 모드 기본 렌더 후 토글로 캔버스 진입, 다시 폼으로 — settings state 유지 확인
|
||||
|
||||
### 12.3 수동 검증 체크리스트
|
||||
|
||||
배포 전 데스크탑 브라우저:
|
||||
- [ ] 토글 폼↔캔버스 전환 시 가중치 동기화
|
||||
- [ ] 캔버스에서 슬라이더 → `dirty` 표시 정상
|
||||
- [ ] `▶ 실행` → 하단 ResultTable 갱신
|
||||
- [ ] 노드 드래그 → 새로고침 후 위치 복원
|
||||
- [ ] `🔄` 리셋 → 초기 위치로 복귀
|
||||
- [ ] 모바일 (DevTools 360×640) → 토글 미표시, 폼 강제
|
||||
|
||||
---
|
||||
|
||||
## 13. 성능
|
||||
|
||||
| 항목 | 평가 |
|
||||
|------|------|
|
||||
| 번들 사이즈 | `@xyflow/react` ~50KB gzipped + 노드 카드 컴포넌트 ~5KB. 전체 web-ui 번들 영향 미미 |
|
||||
| 렌더 비용 | 11개 노드, 16개 엣지 — React Flow 권장 한계 대비 매우 작음 |
|
||||
| localStorage I/O | 노드 드래그 종료(`onNodeDragStop`) 시점에만 write, 드래그 중 빈번한 write 없음 |
|
||||
| 모바일 폴백 | useIsMobile 분기로 캔버스 컴포넌트 자체를 mount하지 않음 → 모바일 번들 부담 없음 (lazy import 검토 가치 있음) |
|
||||
|
||||
`@xyflow/react` 는 데스크탑 진입 시에만 필요하므로 **`React.lazy` + `Suspense` 로 분리 import** 권장 (Plan에서 task로 명시).
|
||||
|
||||
---
|
||||
|
||||
## 14. 후속 슬라이스 후보 (이번 슬라이스 NOT)
|
||||
|
||||
이번 캔버스 슬라이스가 완료된 이후 자연스럽게 이어질 수 있는 작업들:
|
||||
|
||||
1. **노드 추가/삭제 UI** — 캔버스 우클릭 메뉴로 점수 노드 추가/제거 (백엔드 registry 동적 등록 필요)
|
||||
2. **자유 그래프 모드** — 토폴로지 자체를 사용자가 구성 (엔진 재설계 동반)
|
||||
3. **캔버스 안 결과 노드 펼치기** — 결과 노드 클릭 시 in-canvas 결과 표
|
||||
4. **캔버스 백테스트 시각화** — 노드별 기여도 히트맵 (후속 백테스트 슬라이스와 연동)
|
||||
5. **노드 그룹화** — 점수 노드 7개를 묶어 접기/펼치기
|
||||
6. **키보드 단축키** — Space=실행, Cmd+S=저장, R=리셋
|
||||
|
||||
---
|
||||
|
||||
## 15. 리스크와 완화
|
||||
|
||||
| 리스크 | 완화 |
|
||||
|--------|------|
|
||||
| `@xyflow/react` API 변경 (v11 → v12 transition 중) | spec 작성 시점 안정 버전(`12.x`) 고정, package.json에 명시 |
|
||||
| 캔버스 모드에서 폼 모드 settings와 동기화 깨짐 | 같은 hook 인스턴스 공유 + Screener.jsx 한 컴포넌트가 두 layout 분기 렌더 → 동일 state 자동 공유 |
|
||||
| 노드 카드가 너무 커서 캔버스 빽빽 | spec 6장의 카드 폭(220~240px), 점수 노드 세로 90px 간격으로 사전 검증된 좌표 사용 |
|
||||
| localStorage 무한 누적 | 키는 정해진 1개씩만 사용, 마이그레이션 시 키 명에 -v1 suffix |
|
||||
| 모바일 사용자 혼란 | 토글 자체를 렌더하지 않음 → 캔버스 모드 존재 자체를 알지 못함 → 학습 부담 0 |
|
||||
|
||||
---
|
||||
|
||||
## 16. API/백엔드 영향
|
||||
|
||||
**없음**. 본 슬라이스는 프론트엔드 전용. 기존 API:
|
||||
- `GET /api/stock/screener/nodes`
|
||||
- `GET/PUT /api/stock/screener/settings`
|
||||
- `POST /api/stock/screener/run`
|
||||
|
||||
를 그대로 사용한다. settings의 shape도 변경 없음.
|
||||
|
||||
---
|
||||
|
||||
## 17. 배포
|
||||
|
||||
- 프론트만 변경 → `npm run release:nas` 또는 `scripts\deploy.bat --frontend`
|
||||
- 백엔드 배포 불필요
|
||||
- 마이그레이션 불필요 (DB 변경 없음, localStorage는 점진적 적용)
|
||||
|
||||
---
|
||||
|
||||
## 18. 완료 조건 (Definition of Done)
|
||||
|
||||
- [ ] 데스크탑에서 헤더 [폼][캔버스] 토글이 보이고 정상 전환
|
||||
- [ ] 캔버스 모드에 11개 노드, 16개 엣지가 사전 정의된 위치로 표시
|
||||
- [ ] 점수 노드 카드에서 가중치 슬라이더/활성 체크박스/핵심 파라미터 편집 동작
|
||||
- [ ] 카드 ⓘ 호버 시 설명 툴팁 표시, 한 줄 요약 항상 표시
|
||||
- [ ] floating 툴바 4개 버튼 (실행/저장 실행/설정 저장/레이아웃 리셋) 모두 동작
|
||||
- [ ] 노드 드래그 → localStorage 저장 → 새로고침 후 복원
|
||||
- [ ] 🔄 리셋 → 초기 좌표 복귀 + localStorage 삭제
|
||||
- [ ] 모바일 (≤768px)에서 토글 미렌더, 폼 강제
|
||||
- [ ] 폼/캔버스 모드 전환해도 settings, 미리보기 히스토리, 결과 유지
|
||||
- [ ] 12.1의 단위 테스트 모두 통과
|
||||
- [ ] 12.3의 수동 검증 체크리스트 통과
|
||||
Reference in New Issue
Block a user