Files
goldenChart/frontend/src/components/IndicatorSettingsStyleSection.tsx
T
2026-05-28 00:36:49 +09:00

279 lines
9.5 KiB
TypeScript

/**
* 보조지표 설정 — 스타일 탭 (플롯·수평선 색상/굵기/타입)
* 타이틀 열 너비를 섹션 내 최장 라벨에 맞춰 컨트롤 상하 정렬
*/
import React, { useLayoutEffect, useRef } from 'react';
import type { PlotDef, HLineDef, HLineStyle } from '../utils/indicatorRegistry';
import { getHLineLabel } from '../utils/indicatorRegistry';
import { getPlotLabel } from '../utils/indicatorLabels';
import { getHlinePriceSpec } from '../utils/indicatorParamSpec';
import NumericParamInput from './NumericParamInput';
import { ColorInput } from './IndicatorSettingsSections';
import {
LINE_WIDTH_OPTIONS,
LINE_STYLE_OPTIONS,
resolveHistogramUpColor,
resolveHistogramDownColor,
} from '../utils/plotColorUtils';
import { IchimokuCloudSection } from './IndicatorSettingsSections';
import { getIchimokuPlotTitle, type IchimokuCloudColors } from '../utils/ichimokuConfig';
/** 스타일 탭 선 유형 드롭다운 (업비트 표기) */
const LINE_STYLE_TAB_LABELS: Record<HLineStyle, string> = {
solid: '라인',
dashed: '점선',
dotted: '도트',
};
export interface IndicatorSettingsStyleSectionProps {
indicatorType: string;
plots: PlotDef[];
hlines: HLineDef[];
cloudColors?: IchimokuCloudColors;
showIchimokuCloud?: boolean;
onPlotStyle: (idx: number, patch: Partial<PlotDef>) => void;
onHLineStyle: (idx: number, patch: Partial<HLineDef>) => void;
onHLinePrice?: (idx: number, price: number) => void;
onCloudColors?: (patch: Partial<IchimokuCloudColors>) => void;
}
function measureTitleColumn(sectionEl: HTMLElement | null, measureEl: HTMLElement | null) {
if (!sectionEl || !measureEl) return;
let maxW = 0;
measureEl.querySelectorAll<HTMLElement>('.ism-plot-title').forEach(el => {
maxW = Math.max(maxW, el.offsetWidth);
});
if (maxW > 0) {
sectionEl.style.setProperty('--ism-plot-title-width', `${Math.ceil(maxW)}px`);
}
}
const PlotStyleRow: React.FC<{
label: string;
plot: PlotDef;
disabled?: boolean;
onStyle: (patch: Partial<PlotDef>) => void;
}> = ({ label, plot, disabled, onStyle }) => {
const isHistogram = plot.type === 'histogram';
const lineWidth = plot.lineWidth ?? 1;
const lineStyle = plot.lineStyle ?? 'solid';
return (
<div className="ism-style-row">
<div className="ism-plot-title-cell">
<span className="ism-plot-title">{label}</span>
</div>
<div className="ism-style-field">
<span className="ism-style-label">색상</span>
<ColorInput
value={plot.color}
onChange={v => onStyle({ color: v })}
/>
</div>
<div className="ism-style-field">
<span className="ism-style-label">굵기</span>
{isHistogram ? (
<span className="ism-style-field-spacer ism-num-combo-spacer" aria-hidden />
) : (
<select
className="ism-select ism-style-width-select"
value={lineWidth}
disabled={disabled}
onChange={e => onStyle({ lineWidth: Number(e.target.value) })}
>
{LINE_WIDTH_OPTIONS.map(w => (
<option key={w} value={w}>{w}</option>
))}
</select>
)}
</div>
<div className="ism-style-field">
<span className="ism-style-label">타입</span>
{isHistogram ? (
<span className="ism-style-type-static">히스토그램</span>
) : (
<select
className="ism-select ism-style-type-select"
value={lineStyle}
disabled={disabled}
onChange={e => onStyle({ lineStyle: e.target.value as HLineStyle })}
>
{LINE_STYLE_OPTIONS.map(s => (
<option key={s} value={s}>{LINE_STYLE_TAB_LABELS[s]}</option>
))}
</select>
)}
</div>
</div>
);
};
const HLineStyleRow: React.FC<{
indicatorType: string;
label: string;
hl: HLineDef;
onStyle: (patch: Partial<HLineDef>) => void;
onPrice?: (price: number) => void;
}> = ({ indicatorType, label, hl, onStyle, onPrice }) => {
const isOn = hl.visible !== false;
const lineWidth = hl.lineWidth ?? 1;
const lineStyle = hl.lineStyle ?? 'dashed';
return (
<div className={`ism-hline-row${isOn ? '' : ' ism-hline-row--off'}`}>
<div className="ism-hline-name-cell">
<span className="ism-hline-label-text">{label}</span>
</div>
<div className="ism-hline-cell">
{onPrice != null ? (
<NumericParamInput
className="ism-input ism-narrow"
value={hl.price}
spec={getHlinePriceSpec(indicatorType, hl.price)}
disabled={!isOn}
onChange={onPrice}
/>
) : (
<span className="ism-hline-price-readonly">{hl.price}</span>
)}
</div>
<div className="ism-hline-cell ism-hline-cell--style">
<ColorInput value={hl.color} onChange={v => onStyle({ color: v })} />
<select
className="ism-select ism-style-width-select"
value={lineWidth}
disabled={!isOn}
title="굵기"
onChange={e => onStyle({ lineWidth: Number(e.target.value) })}
>
{LINE_WIDTH_OPTIONS.map(w => (
<option key={w} value={w}>{w}</option>
))}
</select>
<select
className="ism-select ism-style-type-select"
value={lineStyle}
disabled={!isOn}
title="타입"
onChange={e => onStyle({ lineStyle: e.target.value as HLineStyle })}
>
{LINE_STYLE_OPTIONS.map(s => (
<option key={s} value={s}>{LINE_STYLE_TAB_LABELS[s]}</option>
))}
</select>
</div>
</div>
);
};
const IndicatorSettingsStyleSection: React.FC<IndicatorSettingsStyleSectionProps> = ({
indicatorType,
plots,
hlines,
cloudColors,
showIchimokuCloud,
onPlotStyle,
onHLineStyle,
onHLinePrice,
onCloudColors,
}) => {
const sectionRef = useRef<HTMLDivElement>(null);
const measureRef = useRef<HTMLDivElement>(null);
const hlinePrices = hlines.map(h => h.price);
const plotLabel = (p: PlotDef) => indicatorType === 'IchimokuCloud'
? getIchimokuPlotTitle(p.id)
: getPlotLabel(p.title);
const titleLabels = [
...plots.map(plotLabel),
...hlines.map(h => h.label ?? getHLineLabel(h.price, hlinePrices)),
];
useLayoutEffect(() => {
measureTitleColumn(sectionRef.current, measureRef.current);
}, [titleLabels.join('\0'), plots.length, hlines.length]);
return (
<div className="ism-section ism-style-tab-section">
<div ref={measureRef} className="ism-plot-title-measure" aria-hidden>
{titleLabels.map((t, i) => (
<span key={i} className="ism-plot-title">{t}</span>
))}
</div>
<div ref={sectionRef} className="ism-style-section">
{plots.map((plot, idx) => (
indicatorType === 'MACD' && plot.type === 'histogram' ? (
<div key={plot.id} className="ism-macd-hist-style-block">
<div className="ism-style-row ism-macd-hist-style-title">
<div className="ism-plot-title-cell">
<span className="ism-plot-title">{plotLabel(plot)}</span>
</div>
</div>
<div className="ism-style-row">
<div className="ism-plot-title-cell">
<span className="ism-plot-title ism-plot-title--sub">0 (양수)</span>
</div>
<div className="ism-style-field ism-style-field--wide">
<span className="ism-style-label">색상</span>
<ColorInput
value={resolveHistogramUpColor(plot)}
onChange={v => onPlotStyle(idx, { histogramUpColor: v, color: v })}
/>
</div>
</div>
<div className="ism-style-row">
<div className="ism-plot-title-cell">
<span className="ism-plot-title ism-plot-title--sub">0 아래 (음수)</span>
</div>
<div className="ism-style-field ism-style-field--wide">
<span className="ism-style-label">색상</span>
<ColorInput
value={resolveHistogramDownColor(plot)}
onChange={v => onPlotStyle(idx, { histogramDownColor: v })}
/>
</div>
</div>
</div>
) : (
<PlotStyleRow
key={plot.id}
label={plotLabel(plot)}
plot={plot}
onStyle={patch => onPlotStyle(idx, patch)}
/>
)
))}
{hlines.length > 0 && (
<>
<hr className="ism-hline-divider" />
<div className="ism-hline-header-row">
<span>이름</span>
<span>가격</span>
<span>색상 · 굵기 · 타입</span>
</div>
{hlines.map((hl, idx) => (
<HLineStyleRow
key={idx}
indicatorType={indicatorType}
label={hl.label ?? getHLineLabel(hl.price, hlinePrices)}
hl={hl}
onStyle={patch => onHLineStyle(idx, patch)}
onPrice={onHLinePrice ? v => onHLinePrice(idx, v) : undefined}
/>
))}
</>
)}
</div>
{showIchimokuCloud && cloudColors && onCloudColors && (
<IchimokuCloudSection cloudColors={cloudColors} onChange={onCloudColors} />
)}
</div>
);
};
export default IndicatorSettingsStyleSection;