goldenChat base source add
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* 보조지표 설정 폼 — 개별 설정 모달·일괄 설정 팝업에서 동일 UI/로직 공유
|
||||
*/
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import type { IndicatorConfig, Timeframe } from '../types';
|
||||
import { getIndicatorDef } from '../utils/indicatorRegistry';
|
||||
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
|
||||
import { getPlotLabel } from '../utils/indicatorLabels';
|
||||
import { getNumericParamSpec } from '../utils/indicatorParamSpec';
|
||||
import NumericParamInput from './NumericParamInput';
|
||||
import SmaPeriodInput from './SmaPeriodInput';
|
||||
import {
|
||||
SMA_DEFAULT_SRC,
|
||||
SMA_MA_COUNT,
|
||||
smaPeriodKey,
|
||||
smaPlotId,
|
||||
} from '../utils/smaConfig';
|
||||
import PlotLineStylePicker from './PlotLineStylePicker';
|
||||
import {
|
||||
GlobalParamRow,
|
||||
IchimokuCloudSection,
|
||||
BollingerSymbolSection,
|
||||
BollingerBackgroundRow,
|
||||
SettingsOutputFooter,
|
||||
UnifiedIndicatorBody,
|
||||
} from './IndicatorSettingsSections';
|
||||
import {
|
||||
createEditorSnapshot,
|
||||
configFromEditorSnapshot,
|
||||
resetConfigToDefaults,
|
||||
type IndicatorSettingsEditorSnapshot,
|
||||
} from '../utils/indicatorSettingsEditor';
|
||||
|
||||
export const ALL_TIMEFRAMES: { tf: Timeframe; label: string }[] = [
|
||||
{ tf: '1m', label: '1분' },
|
||||
{ tf: '3m', label: '3분' },
|
||||
{ tf: '5m', label: '5분' },
|
||||
{ tf: '15m', label: '15분' },
|
||||
{ tf: '30m', label: '30분' },
|
||||
{ tf: '1h', label: '1시간' },
|
||||
{ tf: '4h', label: '4시간' },
|
||||
{ tf: '1D', label: '일봉' },
|
||||
{ tf: '1W', label: '주봉' },
|
||||
{ tf: '1M', label: '월봉' },
|
||||
];
|
||||
|
||||
export interface IndicatorSettingsFormProps {
|
||||
config: IndicatorConfig;
|
||||
chartMarket?: string;
|
||||
/** embedded: 일괄 설정 카드 안 */
|
||||
variant?: 'modal' | 'embedded';
|
||||
/**
|
||||
* modal: 편집 중 부모 setState 없이 ref만 갱신 (포커스 유지).
|
||||
* embedded: 변경 즉시 onChange 호출.
|
||||
*/
|
||||
deferParentSync?: boolean;
|
||||
/** @deprecated 탭 제거 — 통합 화면만 사용 */
|
||||
inputsOnly?: boolean;
|
||||
/** 개별 설정 모달: 지표 전체 표시 여부 */
|
||||
chartHidden?: boolean;
|
||||
onChartHiddenChange?: (showOnChart: boolean) => void;
|
||||
onChange: (updated: IndicatorConfig) => void;
|
||||
}
|
||||
|
||||
function configFingerprint(c: IndicatorConfig): string {
|
||||
return JSON.stringify({
|
||||
id: c.id,
|
||||
params: c.params,
|
||||
plots: c.plots,
|
||||
plotVisibility: c.plotVisibility,
|
||||
hlines: c.hlines,
|
||||
hlinesBackground: c.hlinesBackground,
|
||||
bandBackground: c.bandBackground,
|
||||
cloudColors: c.cloudColors,
|
||||
lastValueVisible: c.lastValueVisible,
|
||||
timeframeVisibility: c.timeframeVisibility,
|
||||
hidden: c.hidden,
|
||||
});
|
||||
}
|
||||
|
||||
const IndicatorSettingsForm: React.FC<IndicatorSettingsFormProps> = ({
|
||||
config,
|
||||
chartMarket = '',
|
||||
variant = 'modal',
|
||||
deferParentSync = variant === 'modal',
|
||||
inputsOnly = false,
|
||||
chartHidden = false,
|
||||
onChartHiddenChange,
|
||||
onChange,
|
||||
}) => {
|
||||
const def = getIndicatorDef(config.type);
|
||||
const isSma = config.type === 'SMA';
|
||||
const isIchimoku = config.type === 'IchimokuCloud';
|
||||
const isBollinger = config.type === 'BollingerBands';
|
||||
|
||||
const [snap, setSnap] = useState(() => createEditorSnapshot(config));
|
||||
const baseRef = useRef(config);
|
||||
const fpRef = useRef(configFingerprint(config));
|
||||
const onChangeRef = useRef(onChange);
|
||||
const internalEmitRef = useRef(false);
|
||||
onChangeRef.current = onChange;
|
||||
|
||||
const emitDraft = useCallback((next: IndicatorSettingsEditorSnapshot) => {
|
||||
const updated = configFromEditorSnapshot(baseRef.current, next);
|
||||
baseRef.current = updated;
|
||||
if (deferParentSync) {
|
||||
onChangeRef.current(updated);
|
||||
return;
|
||||
}
|
||||
internalEmitRef.current = true;
|
||||
onChangeRef.current(updated);
|
||||
}, [deferParentSync]);
|
||||
|
||||
const scheduleDraftEmit = useCallback((next: IndicatorSettingsEditorSnapshot) => {
|
||||
if (deferParentSync) {
|
||||
emitDraft(next);
|
||||
return;
|
||||
}
|
||||
emitDraft(next);
|
||||
}, [deferParentSync, emitDraft]);
|
||||
|
||||
useEffect(() => {
|
||||
if (deferParentSync) return;
|
||||
if (internalEmitRef.current) {
|
||||
internalEmitRef.current = false;
|
||||
fpRef.current = configFingerprint(config);
|
||||
baseRef.current = config;
|
||||
return;
|
||||
}
|
||||
const fp = configFingerprint(config);
|
||||
if (fp !== fpRef.current) {
|
||||
fpRef.current = fp;
|
||||
baseRef.current = config;
|
||||
setSnap(createEditorSnapshot(config));
|
||||
}
|
||||
}, [config, deferParentSync]);
|
||||
|
||||
const emit = useCallback((next: IndicatorSettingsEditorSnapshot) => {
|
||||
setSnap(next);
|
||||
scheduleDraftEmit(next);
|
||||
}, [scheduleDraftEmit]);
|
||||
|
||||
const patch = useCallback((partial: Partial<IndicatorSettingsEditorSnapshot>) => {
|
||||
setSnap(prev => {
|
||||
const next = { ...prev, ...partial };
|
||||
scheduleDraftEmit(next);
|
||||
return next;
|
||||
});
|
||||
}, [scheduleDraftEmit]);
|
||||
|
||||
const { params, plots, plotVis, hlines, hlinesBg, lastValueVisible, timeframeVis, cloudColors, bandBg } = snap;
|
||||
const hasHLines = (def?.hlines ?? []).length > 0 || hlines.length > 0;
|
||||
|
||||
const handleHLineVisible = useCallback((idx: number, visible: boolean) =>
|
||||
patch({ hlines: hlines.map((h, i) => i === idx ? { ...h, visible } : h) }), [hlines, patch]);
|
||||
const handleHLinePrice = useCallback((idx: number, price: number) =>
|
||||
patch({ hlines: hlines.map((h, i) => i === idx ? { ...h, price } : h) }), [hlines, patch]);
|
||||
const handleHLinePlotStyle = useCallback((idx: number, p: Partial<HLineDef>) =>
|
||||
patch({ hlines: hlines.map((h, i) => i === idx ? { ...h, ...p } : h) }), [hlines, patch]);
|
||||
|
||||
const handleParamChange = useCallback((key: string, raw: string | boolean) => {
|
||||
const oldVal = params[key];
|
||||
const defVal = def?.defaultParams?.[key];
|
||||
let nextParams: Record<string, number | string | boolean>;
|
||||
if (typeof oldVal === 'number' || typeof defVal === 'number') {
|
||||
const n = parseFloat(raw as string);
|
||||
const fallback = typeof oldVal === 'number' ? oldVal : (defVal as number);
|
||||
nextParams = { ...params, [key]: isNaN(n) ? fallback : n };
|
||||
} else if (typeof oldVal === 'boolean' || typeof defVal === 'boolean') {
|
||||
nextParams = { ...params, [key]: raw as boolean };
|
||||
} else {
|
||||
nextParams = { ...params, [key]: raw as string };
|
||||
}
|
||||
let nextPlotVis = plotVis;
|
||||
if (key === 'maType' && (config.type === 'CCI' || config.type === 'RSI')) {
|
||||
nextPlotVis = { ...plotVis, plot1: raw !== 'None' };
|
||||
}
|
||||
patch({ params: nextParams, plotVis: nextPlotVis });
|
||||
}, [params, plotVis, def, config.type, patch]);
|
||||
|
||||
const handlePlotStyle = useCallback((idx: number, p: Partial<PlotDef>) => {
|
||||
patch({ plots: plots.map((pl, i) => i === idx ? { ...pl, ...p } : pl) });
|
||||
}, [plots, patch]);
|
||||
|
||||
const handlePlotToggle = useCallback((plotId: string, enabled: boolean) => {
|
||||
patch({ plotVis: { ...plotVis, [plotId]: enabled } });
|
||||
}, [plotVis, patch]);
|
||||
|
||||
const handleSmaPeriod = useCallback((maIndex: number, v: number) => {
|
||||
patch({ params: { ...params, [smaPeriodKey(maIndex)]: v } });
|
||||
}, [params, patch]);
|
||||
|
||||
const handleSmaToggle = useCallback((plotIndex: number, enabled: boolean) => {
|
||||
patch({ plotVis: { ...plotVis, [smaPlotId(plotIndex)]: enabled } });
|
||||
}, [plotVis, patch]);
|
||||
|
||||
const handleDefaults = useCallback(() => {
|
||||
const reset = resetConfigToDefaults(config);
|
||||
baseRef.current = reset;
|
||||
fpRef.current = configFingerprint(reset);
|
||||
const nextSnap = createEditorSnapshot(reset);
|
||||
setSnap(nextSnap);
|
||||
onChange(reset);
|
||||
}, [config, onChange]);
|
||||
|
||||
const footerProps = {
|
||||
lastValueVisible,
|
||||
onLastValueVisible: (v: boolean) => patch({ lastValueVisible: v }),
|
||||
timeframeVis,
|
||||
onTimeframeVis: (tf: Timeframe, v: boolean) =>
|
||||
patch({ timeframeVis: { ...timeframeVis, [tf]: v } }),
|
||||
timeframes: ALL_TIMEFRAMES,
|
||||
chartHidden,
|
||||
onChartHidden: onChartHiddenChange,
|
||||
};
|
||||
|
||||
const sectionClass = variant === 'embedded'
|
||||
? 'ism-section unified-settings-section ibsm-embedded-form'
|
||||
: 'ism-section unified-settings-section';
|
||||
|
||||
return (
|
||||
<div className={sectionClass}>
|
||||
{variant === 'embedded' && (
|
||||
<div className="ibsm-form-toolbar">
|
||||
<button type="button" className="ibsm-form-defaults" onClick={handleDefaults}>
|
||||
기본값
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSma && (
|
||||
<div className="sma-inputs-section">
|
||||
<GlobalParamRow
|
||||
indicatorType="SMA"
|
||||
paramKey="src"
|
||||
value={String(params.src ?? SMA_DEFAULT_SRC)}
|
||||
onChange={(_, raw) => patch({ params: { ...params, src: raw as string } })}
|
||||
/>
|
||||
<div className="ism-sma-divider" />
|
||||
{Array.from({ length: SMA_MA_COUNT }, (_, i) => {
|
||||
const maNum = i + 1;
|
||||
const plotId = smaPlotId(i);
|
||||
const periodKey = smaPeriodKey(maNum);
|
||||
const enabled = plotVis[plotId] !== false;
|
||||
const plot = plots[i];
|
||||
return (
|
||||
<div key={plotId} className={`ism-row ism-sma-ma-row${enabled ? '' : ' ism-sma-ma-off'}`}>
|
||||
<label className="ism-toggle ism-sma-ma-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={e => handleSmaToggle(i, e.target.checked)}
|
||||
/>
|
||||
<span className="ism-toggle-slider" />
|
||||
</label>
|
||||
<label className="ism-label ism-sma-ma-label">{getPlotLabel(`MA${maNum}`)}</label>
|
||||
<div
|
||||
className="ism-control ism-sma-ma-control"
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onPointerDown={e => e.stopPropagation()}
|
||||
>
|
||||
<SmaPeriodInput
|
||||
value={Number(params[periodKey] ?? 1)}
|
||||
disabled={!enabled}
|
||||
onCommit={v => handleSmaPeriod(maNum, v)}
|
||||
/>
|
||||
{plot && !inputsOnly && (
|
||||
<PlotLineStylePicker
|
||||
disabled={!enabled}
|
||||
title={`MA${maNum}`}
|
||||
value={{
|
||||
color: plot.color,
|
||||
lineWidth: plot.lineWidth,
|
||||
lineStyle: plot.lineStyle,
|
||||
}}
|
||||
onChange={p => handlePlotStyle(i, p)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!inputsOnly && <SettingsOutputFooter {...footerProps} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isIchimoku && (
|
||||
<UnifiedIndicatorBody
|
||||
config={config}
|
||||
params={params}
|
||||
plots={plots}
|
||||
plotVis={plotVis}
|
||||
hlines={[]}
|
||||
hlinesBg={hlinesBg}
|
||||
hasHLines={false}
|
||||
lastValueVisible={lastValueVisible}
|
||||
timeframeVis={timeframeVis}
|
||||
timeframes={ALL_TIMEFRAMES}
|
||||
onParamChange={handleParamChange}
|
||||
onPlotToggle={handlePlotToggle}
|
||||
onPlotStyle={handlePlotStyle}
|
||||
onHLineVisible={handleHLineVisible}
|
||||
onHLinePrice={handleHLinePrice}
|
||||
onHLineStyle={handleHLinePlotStyle}
|
||||
onHlinesBg={v => patch({ hlinesBg: v })}
|
||||
onLastValueVisible={v => patch({ lastValueVisible: v })}
|
||||
onTimeframeVis={(tf, v) => patch({ timeframeVis: { ...timeframeVis, [tf]: v } })}
|
||||
chartHidden={chartHidden}
|
||||
onChartHidden={onChartHiddenChange}
|
||||
cloudSection={inputsOnly ? undefined : (
|
||||
<IchimokuCloudSection
|
||||
cloudColors={cloudColors}
|
||||
onChange={p => patch({ cloudColors: { ...cloudColors, ...p } })}
|
||||
/>
|
||||
)}
|
||||
inputsOnly={inputsOnly}
|
||||
showOutputFooter={!inputsOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isSma && !isIchimoku && (
|
||||
<UnifiedIndicatorBody
|
||||
config={config}
|
||||
params={params}
|
||||
plots={plots}
|
||||
plotVis={plotVis}
|
||||
hlines={hlines}
|
||||
hlinesBg={hlinesBg}
|
||||
hasHLines={hasHLines}
|
||||
lastValueVisible={lastValueVisible}
|
||||
timeframeVis={timeframeVis}
|
||||
timeframes={ALL_TIMEFRAMES}
|
||||
onParamChange={handleParamChange}
|
||||
onPlotToggle={handlePlotToggle}
|
||||
onPlotStyle={handlePlotStyle}
|
||||
onHLineVisible={handleHLineVisible}
|
||||
onHLinePrice={handleHLinePrice}
|
||||
onHLineStyle={handleHLinePlotStyle}
|
||||
onHlinesBg={v => patch({ hlinesBg: v })}
|
||||
onLastValueVisible={v => patch({ lastValueVisible: v })}
|
||||
onTimeframeVis={(tf, v) => patch({ timeframeVis: { ...timeframeVis, [tf]: v } })}
|
||||
chartHidden={chartHidden}
|
||||
onChartHidden={onChartHiddenChange}
|
||||
inputsOnly={inputsOnly}
|
||||
showOutputFooter={!inputsOnly && !isBollinger}
|
||||
symbolSection={isBollinger ? (
|
||||
<BollingerSymbolSection
|
||||
symbolMode={String(params.symbolMode ?? 'main')}
|
||||
refSymbol={String(params.refSymbol ?? '')}
|
||||
chartMarket={chartMarket}
|
||||
onChange={p => {
|
||||
const next = { ...params };
|
||||
if (p.symbolMode !== undefined) next.symbolMode = p.symbolMode;
|
||||
if (p.refSymbol !== undefined) next.refSymbol = p.refSymbol;
|
||||
patch({ params: next });
|
||||
}}
|
||||
/>
|
||||
) : undefined}
|
||||
afterPlotsSection={isBollinger ? (
|
||||
<BollingerBackgroundRow value={bandBg} onChange={v => patch({ bandBg: v })} />
|
||||
) : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndicatorSettingsForm;
|
||||
Reference in New Issue
Block a user