diff --git a/src/pages/stock/screener/Screener.css b/src/pages/stock/screener/Screener.css
index 231b358..43e2617 100644
--- a/src/pages/stock/screener/Screener.css
+++ b/src/pages/stock/screener/Screener.css
@@ -49,3 +49,13 @@
margin-bottom: 16px;
}
.screener-card h3 { margin: 0 0 12px 0; font-size: 15px; }
+
+.node-card {
+ background: #0a0f1a;
+ border: 1px solid #1f2937;
+ border-radius: 6px;
+ padding: 10px;
+ font-size: 13px;
+}
+.node-card-header { font-weight: 500; margin-bottom: 6px; }
+.weight-row, .param-row { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
diff --git a/src/pages/stock/screener/components/NodeCard.jsx b/src/pages/stock/screener/components/NodeCard.jsx
new file mode 100644
index 0000000..7626285
--- /dev/null
+++ b/src/pages/stock/screener/components/NodeCard.jsx
@@ -0,0 +1,80 @@
+import React from 'react';
+
+export default function NodeCard({ meta, weight, params, onWeightChange, onParamsChange }) {
+ const enabled = (weight ?? 0) > 0;
+
+ return (
+
+
+
+
+
+
+ 가중치
+ onWeightChange(parseFloat(e.target.value))}
+ style={{ flex: 1 }}
+ />
+ {(weight ?? 0).toFixed(1)}
+
+ {Object.entries(meta.param_schema?.properties || {}).map(([key, prop]) => (
+
onParamsChange({ ...params, [key]: v })}
+ />
+ ))}
+
+
+ );
+}
+
+function ParamRow({ paramKey, prop, value, disabled, onChange }) {
+ const type = prop.type;
+ if (type === 'integer' || type === 'number') {
+ return (
+
+ {paramKey}
+ onChange(type === 'integer' ? parseInt(e.target.value, 10) : parseFloat(e.target.value))}
+ style={{ width: 80 }}
+ />
+
+ );
+ }
+ if (type === 'boolean') {
+ return (
+
+
+
+ );
+ }
+ // object/array는 MVP에서 read-only JSON 표시 (RsRating의 weights 등)
+ return (
+
+ {paramKey}: {JSON.stringify(value)}
+
+ );
+}
diff --git a/src/pages/stock/screener/components/NodePanel.jsx b/src/pages/stock/screener/components/NodePanel.jsx
index bdfac8b..6953e26 100644
--- a/src/pages/stock/screener/components/NodePanel.jsx
+++ b/src/pages/stock/screener/components/NodePanel.jsx
@@ -1,3 +1,21 @@
+import NodeCard from './NodeCard';
+
export default function NodePanel({ meta, weights, params, onWeights, onParams }) {
- return 점수 노드 ({meta.length})
TODO: 노드별 카드 (Task 4.4)
;
+ return (
+
+ 점수 노드 ({meta.length})
+
+ {meta.map((m) => (
+ onWeights({ ...weights, [m.name]: w })}
+ onParamsChange={(p) => onParams({ ...params, [m.name]: p })}
+ />
+ ))}
+
+
+ );
}