From ec3ca5fcfabc6c582f87d833ec124eecdadfc464 Mon Sep 17 00:00:00 2001 From: gahusb Date: Wed, 13 May 2026 23:51:09 +0900 Subject: [PATCH] feat(screener): canvas adds AI news node (12 nodes, 18 edges) --- .../canvas/constants/canvasLayout.js | 7 ++++++- .../canvas/constants/canvasLayout.test.js | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/pages/stock/screener/components/canvas/constants/canvasLayout.js b/src/pages/stock/screener/components/canvas/constants/canvasLayout.js index a2363f1..46d5255 100644 --- a/src/pages/stock/screener/components/canvas/constants/canvasLayout.js +++ b/src/pages/stock/screener/components/canvas/constants/canvasLayout.js @@ -8,6 +8,7 @@ export const NODE_IDS = { RS: 'score-rs-rating', MA: 'score-ma-alignment', VCP: 'score-vcp-lite', + AI_NEWS: 'score-ai-news', COMBINE: 'combine', RESULT: 'result', }; @@ -22,6 +23,7 @@ export const NODE_KIND_MAP = { [NODE_IDS.RS]: 'score', [NODE_IDS.MA]: 'score', [NODE_IDS.VCP]: 'score', + [NODE_IDS.AI_NEWS]: 'score', [NODE_IDS.COMBINE]: 'combine', [NODE_IDS.RESULT]: 'result', }; @@ -35,6 +37,7 @@ export const SCORE_NODE_NAME_MAP = { [NODE_IDS.RS]: 'rs_rating', [NODE_IDS.MA]: 'ma_alignment', [NODE_IDS.VCP]: 'vcp_lite', + [NODE_IDS.AI_NEWS]: 'ai_news', }; // 4단 layout: DATA → GATE → (점수 7개 세로) → COMBINE → RESULT @@ -48,11 +51,12 @@ export const INITIAL_NODE_POSITIONS = { [NODE_IDS.RS]: { x: 480, y: 360 }, [NODE_IDS.MA]: { x: 480, y: 450 }, [NODE_IDS.VCP]: { x: 480, y: 540 }, + [NODE_IDS.AI_NEWS]: { x: 480, y: 630 }, [NODE_IDS.COMBINE]: { x: 800, y: 280 }, [NODE_IDS.RESULT]: { x: 1080, y: 280 }, }; -const SCORE_KEYS = ['FOREIGN','VOLUME','MOMENTUM','HIGH52W','RS','MA','VCP']; +const SCORE_KEYS = ['FOREIGN','VOLUME','MOMENTUM','HIGH52W','RS','MA','VCP','AI_NEWS']; export const EDGES = [ { id: 'e-data-gate', source: NODE_IDS.DATA, target: NODE_IDS.GATE }, @@ -77,4 +81,5 @@ export const SCORE_NODE_LABEL = { [NODE_IDS.RS]: { icon: '💪', title: 'RS Rating' }, [NODE_IDS.MA]: { icon: '📈', title: '이평선 정렬' }, [NODE_IDS.VCP]: { icon: '🌀', title: 'VCP-lite' }, + [NODE_IDS.AI_NEWS]: { icon: '🤖', title: 'AI 뉴스' }, }; diff --git a/src/pages/stock/screener/components/canvas/constants/canvasLayout.test.js b/src/pages/stock/screener/components/canvas/constants/canvasLayout.test.js index b3e6234..c507768 100644 --- a/src/pages/stock/screener/components/canvas/constants/canvasLayout.test.js +++ b/src/pages/stock/screener/components/canvas/constants/canvasLayout.test.js @@ -5,10 +5,10 @@ import { } from './canvasLayout'; describe('canvasLayout', () => { - it('NODE_IDS — 11개 키, 모두 unique', () => { + it('NODE_IDS — 12개 키, 모두 unique', () => { const ids = Object.values(NODE_IDS); - expect(ids).toHaveLength(11); - expect(new Set(ids).size).toBe(11); + expect(ids).toHaveLength(12); + expect(new Set(ids).size).toBe(12); }); it('INITIAL_NODE_POSITIONS — 모든 NODE_IDS에 좌표 존재', () => { @@ -20,8 +20,8 @@ describe('canvasLayout', () => { } }); - it('EDGES — 16개, source/target이 모두 NODE_IDS 안에 존재', () => { - expect(EDGES).toHaveLength(16); + it('EDGES — 18개, source/target이 모두 NODE_IDS 안에 존재', () => { + expect(EDGES).toHaveLength(18); const validIds = new Set(Object.values(NODE_IDS)); for (const e of EDGES) { expect(validIds.has(e.source)).toBe(true); @@ -30,10 +30,11 @@ describe('canvasLayout', () => { } }); - it('EDGES — 7개 점수 노드는 모두 gate 입력 + combine 출력을 가짐', () => { + it('EDGES — 8개 점수 노드는 모두 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, + NODE_IDS.AI_NEWS, ]; for (const sid of SCORE_IDS) { const hasGateInput = EDGES.some( @@ -54,9 +55,10 @@ describe('canvasLayout', () => { } }); - it('SCORE_NODE_NAME_MAP — 7개 점수 노드 ID → backend node name', () => { - expect(Object.keys(SCORE_NODE_NAME_MAP)).toHaveLength(7); + it('SCORE_NODE_NAME_MAP — 8개 점수 노드 ID → backend node name', () => { + expect(Object.keys(SCORE_NODE_NAME_MAP)).toHaveLength(8); expect(SCORE_NODE_NAME_MAP[NODE_IDS.FOREIGN]).toBe('foreign_buy'); expect(SCORE_NODE_NAME_MAP[NODE_IDS.VOLUME]).toBe('volume_surge'); + expect(SCORE_NODE_NAME_MAP[NODE_IDS.AI_NEWS]).toBe('ai_news'); }); });