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,597 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
Button,
Box,
Typography,
TextField,
Switch,
Slider,
Divider,
Paper,
Grid,
IconButton,
Tooltip,
Alert,
Select,
MenuItem,
FormControl,
} from '@mui/material';
import { Close, Refresh } from '@mui/icons-material';
import { useIndicatorVisibility } from '../contexts/IndicatorVisibilityContext';
import { useChartColors, ChartColorSettings } from '../contexts/ChartColorContext';
import type { MultiChartBollingerState } from '../stores/useDashboardStore';
import {
bollingerDialogDefaultsFromStore,
bollingerDialogToStore,
bollingerStoreToDialogColors,
bollingerStoreToDialogSettings,
} from '../utils/multiChartToolbarSettings';
import DraggablePaper from './DraggablePaper';
interface BollingerSettingsDialogProps {
open: boolean;
onClose: () => void;
multiChartToolbar?: {
bollinger: MultiChartBollingerState;
setBollinger: (
value: MultiChartBollingerState | ((prev: MultiChartBollingerState) => MultiChartBollingerState)
) => void;
};
}
const BollingerSettingsDialog: React.FC<BollingerSettingsDialogProps> = ({ open, onClose, multiChartToolbar }) => {
const { settings, updateSettings } = useIndicatorVisibility();
const { colors, updateColors } = useChartColors();
// 로컬 상태 (볼린저 밴드 관련 설정만 포함)
const [localSettings, setLocalSettings] = useState<{
bollingerUpper: boolean;
bollingerMiddle: boolean;
bollingerLower: boolean;
bollingerPeriod: number;
bollingerStdDev: number;
}>({
bollingerUpper: settings.bollingerUpper,
bollingerMiddle: settings.bollingerMiddle,
bollingerLower: settings.bollingerLower,
bollingerPeriod: settings.bollingerPeriod || 20,
bollingerStdDev: settings.bollingerStdDev || 2.0,
});
const [localColors, setLocalColors] = useState({
bollingerUpperColor: colors.bollingerUpperColor,
bollingerMiddleColor: colors.bollingerMiddleColor,
bollingerLowerColor: colors.bollingerLowerColor,
bollingerUpperWidth: colors.bollingerUpperWidth,
bollingerMiddleWidth: colors.bollingerMiddleWidth,
bollingerLowerWidth: colors.bollingerLowerWidth,
bollingerUpperLineStyle: colors.bollingerUpperLineStyle,
bollingerMiddleLineStyle: colors.bollingerMiddleLineStyle,
bollingerLowerLineStyle: colors.bollingerLowerLineStyle,
bollingerBandBackgroundColor: colors.bollingerBandBackgroundColor,
});
// 배경 투명도 상태
const [bandOpacity, setBandOpacity] = useState<number>(0.15); // ✅ 기본값 증가 (더 잘 보이도록)
// rgba에서 alpha 값 추출 함수
const extractOpacity = (color: string): number => {
const match = color.match(/rgba?\([^,]+,[^,]+,[^,]+,\s*([\d.]+)\)/);
return match ? parseFloat(match[1]) : 0.1;
};
// rgba에서 rgb hex 추출 함수
const extractRgbFromRgba = (rgba: string): string => {
const match = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (match) {
const r = parseInt(match[1]).toString(16).padStart(2, '0');
const g = parseInt(match[2]).toString(16).padStart(2, '0');
const b = parseInt(match[3]).toString(16).padStart(2, '0');
return `#${r}${g}${b}`;
}
return '#000000';
};
// hex to rgba 변환 함수
const hexToRgba = (hex: string, opacity: number): string => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (result) {
const r = parseInt(result[1], 16);
const g = parseInt(result[2], 16);
const b = parseInt(result[3], 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
return `rgba(0, 0, 0, ${opacity})`;
};
useEffect(() => {
if (!open || !multiChartToolbar) return;
const b = multiChartToolbar.bollinger;
setLocalSettings(bollingerStoreToDialogSettings(b));
setLocalColors(bollingerStoreToDialogColors(b));
setBandOpacity(extractOpacity(b.bandBackgroundColor));
}, [open, multiChartToolbar]);
useEffect(() => {
if (!open || multiChartToolbar) return;
setLocalSettings({
bollingerUpper: settings.bollingerUpper,
bollingerMiddle: settings.bollingerMiddle,
bollingerLower: settings.bollingerLower,
bollingerPeriod: settings.bollingerPeriod || 20,
bollingerStdDev: settings.bollingerStdDev || 2.0,
});
setLocalColors({
bollingerUpperColor: colors.bollingerUpperColor,
bollingerMiddleColor: colors.bollingerMiddleColor,
bollingerLowerColor: colors.bollingerLowerColor,
bollingerUpperWidth: colors.bollingerUpperWidth,
bollingerMiddleWidth: colors.bollingerMiddleWidth,
bollingerLowerWidth: colors.bollingerLowerWidth,
bollingerBandBackgroundColor: colors.bollingerBandBackgroundColor,
bollingerUpperLineStyle: colors.bollingerUpperLineStyle,
bollingerMiddleLineStyle: colors.bollingerMiddleLineStyle,
bollingerLowerLineStyle: colors.bollingerLowerLineStyle,
});
setBandOpacity(extractOpacity(colors.bollingerBandBackgroundColor));
}, [open, multiChartToolbar, settings, colors]);
const handleSave = () => {
if (multiChartToolbar) {
multiChartToolbar.setBollinger(bollingerDialogToStore(localSettings, localColors));
} else {
console.log('💾 [볼린저밴드 설정] 저장 시작:', {
: {
period: settings.bollingerPeriod,
stdDev: settings.bollingerStdDev,
upper: settings.bollingerUpper,
middle: settings.bollingerMiddle,
lower: settings.bollingerLower,
},
새설정: localSettings,
});
updateSettings({
...settings,
...localSettings,
});
updateColors(localColors);
console.log('✅ [볼린저밴드 설정] updateSettings 및 updateColors 호출 완료');
}
onClose();
};
const handleReset = () => {
if (multiChartToolbar) {
const { localSettings: ls, localColors: lc } = bollingerDialogDefaultsFromStore();
setLocalSettings(ls);
setLocalColors(lc);
setBandOpacity(extractOpacity(lc.bollingerBandBackgroundColor));
} else {
setLocalSettings({
bollingerUpper: false,
bollingerMiddle: false,
bollingerLower: false,
bollingerPeriod: 20,
bollingerStdDev: 2.0,
});
setLocalColors({
bollingerUpperColor: '#FF9800',
bollingerMiddleColor: '#2196F3',
bollingerLowerColor: '#4CAF50',
bollingerUpperWidth: 1.5,
bollingerMiddleWidth: 1.5,
bollingerLowerWidth: 1.5,
bollingerUpperLineStyle: 'solid' as const,
bollingerMiddleLineStyle: 'solid' as const,
bollingerLowerLineStyle: 'solid' as const,
bollingerBandBackgroundColor: 'rgba(33, 150, 243, 0.15)',
});
setBandOpacity(0.15);
}
};
const handlePeriodChange = (value: number) => {
setLocalSettings({ ...localSettings, bollingerPeriod: value });
};
const handleStdDevChange = (value: number) => {
setLocalSettings({ ...localSettings, bollingerStdDev: value });
};
// 색상 테이블 컴포넌트
const ColorTable = () => {
const configs: Array<{
label: string;
enableKey: 'bollingerUpper' | 'bollingerMiddle' | 'bollingerLower';
colorKey: 'bollingerUpperColor' | 'bollingerMiddleColor' | 'bollingerLowerColor';
widthKey: 'bollingerUpperWidth' | 'bollingerMiddleWidth' | 'bollingerLowerWidth';
lineStyleKey: 'bollingerUpperLineStyle' | 'bollingerMiddleLineStyle' | 'bollingerLowerLineStyle';
}> = [
{
label: '상한선',
enableKey: 'bollingerUpper',
colorKey: 'bollingerUpperColor',
widthKey: 'bollingerUpperWidth',
lineStyleKey: 'bollingerUpperLineStyle'
},
{
label: '중간선',
enableKey: 'bollingerMiddle',
colorKey: 'bollingerMiddleColor',
widthKey: 'bollingerMiddleWidth',
lineStyleKey: 'bollingerMiddleLineStyle'
},
{
label: '하한선',
enableKey: 'bollingerLower',
colorKey: 'bollingerLowerColor',
widthKey: 'bollingerLowerWidth',
lineStyleKey: 'bollingerLowerLineStyle'
},
];
return (
<Box sx={{ mb: 0.5 }}>
<Typography variant="caption" fontWeight="bold" color="text.secondary" sx={{ display: 'block', mb: 1, fontSize: '12px' }}>
🎨 , ,
</Typography>
{/* 헤더 행 */}
<Box sx={{
p: 1,
mb: 0.5,
bgcolor: (theme) => theme.palette.mode === 'dark' ? '#616161' : '#9e9e9e',
borderRadius: 1,
}}>
<Grid container spacing={1.5} alignItems="center">
<Grid item xs={1}>
<Typography variant="caption" fontWeight="bold" color="white">ON/OFF</Typography>
</Grid>
<Grid item xs={1.5}>
<Typography variant="caption" fontWeight="bold" color="white"></Typography>
</Grid>
<Grid item xs={1.5}>
<Typography variant="caption" fontWeight="bold" color="white"></Typography>
</Grid>
<Grid item xs={2.5}>
<Typography variant="caption" fontWeight="bold" color="white"></Typography>
</Grid>
<Grid item xs={2.5}>
<Typography variant="caption" fontWeight="bold" color="white"> </Typography>
</Grid>
<Grid item xs={3}>
<Typography variant="caption" fontWeight="bold" color="white"></Typography>
</Grid>
</Grid>
</Box>
{/* 각 색상 설정 행 */}
{configs.map((cfg) => {
const colorValue = String(localColors[cfg.colorKey] || '#666666');
const widthValue = Number(localColors[cfg.widthKey] ?? 1.5);
const lineStyleValue = String(localColors[cfg.lineStyleKey] ?? 'solid');
const enabledValue = Boolean(localSettings[cfg.enableKey]);
return (
<Box key={cfg.colorKey} sx={{ p: 1, mb: 0.5, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}>
<Grid container spacing={1} alignItems="center">
{/* ON/OFF 토글 */}
<Grid item xs={1} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Switch
checked={enabledValue}
onChange={(e) => setLocalSettings({ ...localSettings, [cfg.enableKey]: e.target.checked })}
size="small"
/>
</Grid>
{/* 항목 이름 */}
<Grid item xs={1.5} sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body2" sx={{ fontSize: '12px', fontWeight: 'medium' }}>
{cfg.label}
</Typography>
</Grid>
{/* 색상 선택 */}
<Grid item xs={1.5} sx={{ display: 'flex', alignItems: 'center' }}>
<input
type="color"
value={colorValue}
onChange={(e) => setLocalColors({ ...localColors, [cfg.colorKey]: e.target.value })}
style={{
width: '100%',
height: '30px',
border: '1px solid rgba(0, 0, 0, 0.23)',
borderRadius: '4px',
cursor: 'pointer',
backgroundColor: colorValue,
}}
/>
</Grid>
{/* 굵기 슬라이더 */}
<Grid item xs={2.5} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Slider
value={widthValue}
onChange={(_, value) => setLocalColors({ ...localColors, [cfg.widthKey]: value as number })}
min={1}
max={5}
step={0.5}
size="small"
sx={{ flex: 1 }}
/>
<Typography variant="caption" sx={{ minWidth: 35, textAlign: 'right', fontSize: '11px' }}>
{widthValue}px
</Typography>
</Grid>
{/* 선 유형 */}
<Grid item xs={2.5}>
<FormControl size="small" fullWidth>
<Select
value={lineStyleValue}
onChange={(e) => setLocalColors({ ...localColors, [cfg.lineStyleKey]: e.target.value as 'solid' | 'dashed' | 'dotted' })}
sx={{ height: 30, fontSize: '12px' }}
>
<MenuItem value="solid" sx={{ fontSize: '12px' }}></MenuItem>
<MenuItem value="dashed" sx={{ fontSize: '12px' }}></MenuItem>
<MenuItem value="dotted" sx={{ fontSize: '12px' }}></MenuItem>
</Select>
</FormControl>
</Grid>
{/* 미리보기 */}
<Grid item xs={3} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: 30 }}>
<svg width="100%" height="100%" style={{ display: 'block' }}>
<line
x1="0"
y1="50%"
x2="100%"
y2="50%"
stroke={colorValue}
strokeWidth={widthValue}
strokeDasharray={lineStyleValue === 'dashed' ? '5,5' : lineStyleValue === 'dotted' ? '2,3' : '0'}
/>
</svg>
</Grid>
</Grid>
</Box>
);
})}
</Box>
);
};
return (
<Dialog
open={open}
onClose={onClose}
PaperComponent={DraggablePaper}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
maxHeight: '90vh',
},
}}
>
<DialogTitle sx={{
cursor: 'move',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
bgcolor: '#1976d2',
color: '#fff',
py: 1.2,
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px', color: '#fff' }}>
</Typography>
<Tooltip title="초기화">
<IconButton size="small" onClick={handleReset} sx={{ color: '#fff', '&:hover': { bgcolor: 'rgba(255,255,255,0.1)' } }}>
<Refresh />
</IconButton>
</Tooltip>
</Box>
<Box
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
onMouseDown={(e) => e.stopPropagation()}
>
<Button
onClick={(e) => {
e.stopPropagation();
onClose();
}}
sx={{
color: '#fff',
borderColor: '#fff',
'&:hover': {
bgcolor: 'rgba(255,255,255,0.1)',
borderColor: '#fff'
},
minWidth: '70px',
fontSize: '13px',
fontWeight: 600,
}}
variant="outlined"
size="small"
>
</Button>
<Button
onClick={(e) => {
e.stopPropagation();
handleSave();
}}
sx={{
bgcolor: '#fff',
color: '#1976d2',
'&:hover': {
bgcolor: 'rgba(255,255,255,0.9)'
},
minWidth: '70px',
fontSize: '13px',
fontWeight: 600,
}}
variant="contained"
size="small"
>
</Button>
</Box>
</DialogTitle>
<DialogContent sx={{ pt: 3 }}>
<Alert severity="info" sx={{ mb: 3 }}>
. .
</Alert>
{/* 기간 설정 */}
<Paper sx={{ p: 2, mb: 2, border: '1px solid', borderColor: 'divider', bgcolor: 'action.hover' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 2 }}>
</Typography>
<Box sx={{ mb: 3 }}>
<Typography variant="caption" sx={{ mb: 0.5, display: 'block' }}>
(Period): {localSettings.bollingerPeriod}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Slider
value={localSettings.bollingerPeriod}
onChange={(_, value) => handlePeriodChange(value as number)}
min={5}
max={50}
step={1}
marks={[
{ value: 10, label: '10' },
{ value: 20, label: '20' },
{ value: 30, label: '30' },
{ value: 40, label: '40' },
]}
valueLabelDisplay="auto"
sx={{ flex: 1 }}
/>
<TextField
type="number"
value={localSettings.bollingerPeriod}
onChange={(e) => handlePeriodChange(Math.max(5, Math.min(50, Number(e.target.value))))}
size="small"
sx={{ width: 70 }}
inputProps={{ min: 5, max: 50 }}
/>
</Box>
</Box>
<Box>
<Typography variant="caption" sx={{ mb: 0.5, display: 'block' }}>
(StdDev): {localSettings.bollingerStdDev}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Slider
value={localSettings.bollingerStdDev}
onChange={(_, value) => handleStdDevChange(value as number)}
min={1.0}
max={3.0}
step={0.1}
marks={[
{ value: 1.5, label: '1.5' },
{ value: 2.0, label: '2.0' },
{ value: 2.5, label: '2.5' },
]}
valueLabelDisplay="auto"
sx={{ flex: 1 }}
/>
<TextField
type="number"
value={localSettings.bollingerStdDev}
onChange={(e) => handleStdDevChange(Math.max(1.0, Math.min(3.0, Number(e.target.value))))}
size="small"
sx={{ width: 70 }}
inputProps={{ min: 1.0, max: 3.0, step: 0.1 }}
/>
</Box>
</Box>
</Paper>
{/* 색상 및 선 설정 테이블 */}
<ColorTable />
{/* 배경색 설정 */}
<Paper sx={{ p: 2, mt: 2, border: '1px solid', borderColor: 'divider', bgcolor: 'action.hover' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 2 }}>
</Typography>
<Box sx={{ mb: 2 }}>
<Grid container spacing={2} alignItems="center">
<Grid item xs={3}>
<Typography variant="body2"> </Typography>
</Grid>
<Grid item xs={9}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Box
sx={{
width: 40,
height: 40,
borderRadius: 1,
border: '1px solid rgba(0, 0, 0, 0.23)',
position: 'relative',
cursor: 'pointer',
backgroundColor: localColors.bollingerBandBackgroundColor,
}}
>
<input
type="color"
value={extractRgbFromRgba(localColors.bollingerBandBackgroundColor)}
onChange={(e) => {
const rgbaColor = hexToRgba(e.target.value, bandOpacity);
setLocalColors({ ...localColors, bollingerBandBackgroundColor: rgbaColor });
}}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
opacity: 0,
cursor: 'pointer',
}}
/>
</Box>
<Typography variant="body2" sx={{ fontFamily: 'monospace', fontSize: '12px', minWidth: 160 }}>
{localColors.bollingerBandBackgroundColor}
</Typography>
</Box>
</Grid>
</Grid>
</Box>
<Box>
<Typography variant="body2" sx={{ mb: 1 }}>
: {(bandOpacity * 100).toFixed(0)}%
</Typography>
<Slider
value={bandOpacity}
onChange={(_, value) => {
const newOpacity = value as number;
setBandOpacity(newOpacity);
const baseColor = extractRgbFromRgba(localColors.bollingerBandBackgroundColor);
const rgbaColor = hexToRgba(baseColor, newOpacity);
setLocalColors({ ...localColors, bollingerBandBackgroundColor: rgbaColor });
}}
min={0}
max={1}
step={0.05}
valueLabelDisplay="auto"
valueLabelFormat={(value) => `${(value * 100).toFixed(0)}%`}
sx={{ width: '100%' }}
/>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
</Typography>
</Paper>
</DialogContent>
</Dialog>
);
};
export default BollingerSettingsDialog;