goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -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;