feat(stock): NodeCard 자동 폼 (param_schema 기반) + NodePanel 통합
This commit is contained in:
@@ -49,3 +49,13 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.screener-card h3 { margin: 0 0 12px 0; font-size: 15px; }
|
.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; }
|
||||||
|
|||||||
80
src/pages/stock/screener/components/NodeCard.jsx
Normal file
80
src/pages/stock/screener/components/NodeCard.jsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function NodeCard({ meta, weight, params, onWeightChange, onParamsChange }) {
|
||||||
|
const enabled = (weight ?? 0) > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="node-card" style={{ opacity: enabled ? 1 : 0.6 }}>
|
||||||
|
<div className="node-card-header">
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={(e) => onWeightChange(e.target.checked ? (weight || 1) : 0)}
|
||||||
|
/>
|
||||||
|
<span>{meta.label}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="node-card-body">
|
||||||
|
<div className="weight-row">
|
||||||
|
<span style={{ width: 50, fontSize: 12, color: '#9ca3af' }}>가중치</span>
|
||||||
|
<input
|
||||||
|
type="range" min="0" max="3" step="0.1"
|
||||||
|
value={weight ?? 0}
|
||||||
|
disabled={!enabled}
|
||||||
|
onChange={(e) => onWeightChange(parseFloat(e.target.value))}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<span style={{ width: 32, textAlign: 'right', fontSize: 12 }}>{(weight ?? 0).toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
{Object.entries(meta.param_schema?.properties || {}).map(([key, prop]) => (
|
||||||
|
<ParamRow
|
||||||
|
key={key}
|
||||||
|
paramKey={key}
|
||||||
|
prop={prop}
|
||||||
|
value={params?.[key] ?? meta.default_params?.[key]}
|
||||||
|
disabled={!enabled}
|
||||||
|
onChange={(v) => onParamsChange({ ...params, [key]: v })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ParamRow({ paramKey, prop, value, disabled, onChange }) {
|
||||||
|
const type = prop.type;
|
||||||
|
if (type === 'integer' || type === 'number') {
|
||||||
|
return (
|
||||||
|
<div className="param-row">
|
||||||
|
<span style={{ width: 100, fontSize: 12 }}>{paramKey}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={prop.minimum} max={prop.maximum}
|
||||||
|
step={type === 'integer' ? 1 : 0.1}
|
||||||
|
value={value ?? ''}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => onChange(type === 'integer' ? parseInt(e.target.value, 10) : parseFloat(e.target.value))}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'boolean') {
|
||||||
|
return (
|
||||||
|
<div className="param-row">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" checked={!!value} disabled={disabled}
|
||||||
|
onChange={(e) => onChange(e.target.checked)} />
|
||||||
|
<span style={{ marginLeft: 6, fontSize: 12 }}>{paramKey}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// object/array는 MVP에서 read-only JSON 표시 (RsRating의 weights 등)
|
||||||
|
return (
|
||||||
|
<div className="param-row" style={{ fontSize: 11, color: '#9ca3af' }}>
|
||||||
|
{paramKey}: <code>{JSON.stringify(value)}</code>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,21 @@
|
|||||||
|
import NodeCard from './NodeCard';
|
||||||
|
|
||||||
export default function NodePanel({ meta, weights, params, onWeights, onParams }) {
|
export default function NodePanel({ meta, weights, params, onWeights, onParams }) {
|
||||||
return <section className="screener-card"><h3>점수 노드 ({meta.length})</h3><p style={{fontSize: 12, color:'#9ca3af'}}>TODO: 노드별 카드 (Task 4.4)</p></section>;
|
return (
|
||||||
|
<section className="screener-card">
|
||||||
|
<h3>점수 노드 ({meta.length})</h3>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
{meta.map((m) => (
|
||||||
|
<NodeCard
|
||||||
|
key={m.name}
|
||||||
|
meta={m}
|
||||||
|
weight={weights[m.name]}
|
||||||
|
params={params[m.name]}
|
||||||
|
onWeightChange={(w) => onWeights({ ...weights, [m.name]: w })}
|
||||||
|
onParamsChange={(p) => onParams({ ...params, [m.name]: p })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user