feat(screener): add canvas layout constants (11 nodes, 16 edges)

This commit is contained in:
2026-05-13 21:35:27 +09:00
parent 5f95f55271
commit c60c32b7f2
2 changed files with 142 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
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 NODE_KIND_MAP = {
[NODE_IDS.DATA]: 'data',
[NODE_IDS.GATE]: 'gate',
[NODE_IDS.FOREIGN]: 'score',
[NODE_IDS.VOLUME]: 'score',
[NODE_IDS.MOMENTUM]: 'score',
[NODE_IDS.HIGH52W]: 'score',
[NODE_IDS.RS]: 'score',
[NODE_IDS.MA]: 'score',
[NODE_IDS.VCP]: 'score',
[NODE_IDS.COMBINE]: 'combine',
[NODE_IDS.RESULT]: 'result',
};
// 캔버스 노드 ID → 백엔드 score node name (registry 키)
export const SCORE_NODE_NAME_MAP = {
[NODE_IDS.FOREIGN]: 'foreign_buy',
[NODE_IDS.VOLUME]: 'volume_surge',
[NODE_IDS.MOMENTUM]: 'momentum',
[NODE_IDS.HIGH52W]: 'high52w',
[NODE_IDS.RS]: 'rs_rating',
[NODE_IDS.MA]: 'ma_alignment',
[NODE_IDS.VCP]: 'vcp_lite',
};
// 4단 layout: DATA → GATE → (점수 7개 세로) → COMBINE → 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 },
};
const SCORE_KEYS = ['FOREIGN','VOLUME','MOMENTUM','HIGH52W','RS','MA','VCP'];
export const EDGES = [
{ id: 'e-data-gate', source: NODE_IDS.DATA, target: NODE_IDS.GATE },
...SCORE_KEYS.map((k) => ({
id: `e-gate-${k.toLowerCase()}`,
source: NODE_IDS.GATE,
target: NODE_IDS[k],
})),
...SCORE_KEYS.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 },
];
export const SCORE_NODE_LABEL = {
[NODE_IDS.FOREIGN]: { icon: '🌏', title: '외국인 매수' },
[NODE_IDS.VOLUME]: { icon: '📊', title: '거래량 급증' },
[NODE_IDS.MOMENTUM]: { icon: '🚀', title: '모멘텀' },
[NODE_IDS.HIGH52W]: { icon: '🔝', title: '52주 고가' },
[NODE_IDS.RS]: { icon: '💪', title: 'RS Rating' },
[NODE_IDS.MA]: { icon: '📈', title: '이평선 정렬' },
[NODE_IDS.VCP]: { icon: '🌀', title: 'VCP-lite' },
};

View File

@@ -0,0 +1,62 @@
import { describe, it, expect } from 'vitest';
import {
NODE_IDS, INITIAL_NODE_POSITIONS, EDGES,
NODE_KIND_MAP, SCORE_NODE_NAME_MAP,
} from './canvasLayout';
describe('canvasLayout', () => {
it('NODE_IDS — 11개 키, 모두 unique', () => {
const ids = Object.values(NODE_IDS);
expect(ids).toHaveLength(11);
expect(new Set(ids).size).toBe(11);
});
it('INITIAL_NODE_POSITIONS — 모든 NODE_IDS에 좌표 존재', () => {
for (const id of Object.values(NODE_IDS)) {
expect(INITIAL_NODE_POSITIONS[id]).toMatchObject({
x: expect.any(Number),
y: expect.any(Number),
});
}
});
it('EDGES — 16개, source/target이 모두 NODE_IDS 안에 존재', () => {
expect(EDGES).toHaveLength(16);
const validIds = new Set(Object.values(NODE_IDS));
for (const e of EDGES) {
expect(validIds.has(e.source)).toBe(true);
expect(validIds.has(e.target)).toBe(true);
expect(e.id).toBeTruthy();
}
});
it('EDGES — 7개 점수 노드는 모두 gate 입력 + combine 출력을 가짐', () => {
const SCORE_IDS = [
NODE_IDS.FOREIGN, NODE_IDS.VOLUME, NODE_IDS.MOMENTUM,
NODE_IDS.HIGH52W, NODE_IDS.RS, NODE_IDS.MA, NODE_IDS.VCP,
];
for (const sid of SCORE_IDS) {
const hasGateInput = EDGES.some(
(e) => e.source === NODE_IDS.GATE && e.target === sid
);
const hasCombineOutput = EDGES.some(
(e) => e.source === sid && e.target === NODE_IDS.COMBINE
);
expect(hasGateInput).toBe(true);
expect(hasCombineOutput).toBe(true);
}
});
it('NODE_KIND_MAP — 각 노드의 kind ∈ {data,gate,score,combine,result}', () => {
const valid = new Set(['data','gate','score','combine','result']);
for (const id of Object.values(NODE_IDS)) {
expect(valid.has(NODE_KIND_MAP[id])).toBe(true);
}
});
it('SCORE_NODE_NAME_MAP — 7개 점수 노드 ID → backend node name', () => {
expect(Object.keys(SCORE_NODE_NAME_MAP)).toHaveLength(7);
expect(SCORE_NODE_NAME_MAP[NODE_IDS.FOREIGN]).toBe('foreign_buy');
expect(SCORE_NODE_NAME_MAP[NODE_IDS.VOLUME]).toBe('volume_surge');
});
});