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,562 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Box,
Typography,
Switch,
FormControlLabel,
Divider,
useTheme,
useMediaQuery,
Alert,
Paper,
TextField,
Slider,
IconButton,
} from '@mui/material';
import { DragIndicator, Close } from '@mui/icons-material';
import { useChartColors, ChartColorSettings } from '../contexts/ChartColorContext';
import { useIndicatorSettings, IndicatorSettings } from '../contexts/IndicatorSettingsContext';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
TouchSensor,
useSensor,
useSensors,
DragEndEvent,
DragStartEvent,
DragOverlay,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useIndicatorVisibility, IndicatorVisibilitySettings, IndicatorOrder } from '../contexts/IndicatorVisibilityContext';
import { useAuth } from '../contexts/AuthContext';
import DraggablePaper from './DraggablePaper';
interface IndicatorSettingsDialogProps {
open: boolean;
onClose: () => void;
}
// 지표별 색상/굵기 설정 키 매핑 (여러 색상 지원)
interface ColorConfig {
label: string;
colorKey: keyof ChartColorSettings;
widthKey?: keyof ChartColorSettings;
}
const indicatorColorConfigs: Record<string, ColorConfig[]> = {
macdLine: [
{ label: 'MACD', colorKey: 'macdLine', widthKey: 'macdLineWidth' },
{ label: '시그널', colorKey: 'signalLine', widthKey: 'signalLineWidth' },
],
rsi: [
{ label: '', colorKey: 'rsiColor', widthKey: 'rsiWidth' },
],
stochasticK: [
{ label: '%K', colorKey: 'stochasticK', widthKey: 'stochasticKWidth' },
{ label: '%D', colorKey: 'stochasticD', widthKey: 'stochasticDWidth' },
],
cci: [
{ label: '', colorKey: 'cciColor', widthKey: 'cciWidth' },
],
adx: [
{ label: '', colorKey: 'adxColor', widthKey: 'adxWidth' },
],
plusDi: [
{ label: '+DI', colorKey: 'plusDiColor', widthKey: 'plusDiWidth' },
{ label: '-DI', colorKey: 'minusDiColor', widthKey: 'minusDiWidth' },
],
minusDi: [], // plusDi에서 함께 처리
williamsR: [
{ label: '', colorKey: 'williamsRColor', widthKey: 'williamsRWidth' },
],
trix: [
{ label: 'TRIX', colorKey: 'trixColor', widthKey: 'trixWidth' },
{ label: '시그널', colorKey: 'trixSignalColor', widthKey: 'trixSignalWidth' },
],
newPsychological: [
{ label: '', colorKey: 'newPsychologicalColor', widthKey: 'newPsychologicalWidth' },
],
investPsychological: [
{ label: '', colorKey: 'investPsychologicalColor', widthKey: 'investPsychologicalWidth' },
],
bwi: [
{ label: '', colorKey: 'bwiColor', widthKey: 'bwiWidth' },
],
obv: [
{ label: '', colorKey: 'obvColor', widthKey: 'obvWidth' },
],
volumeOsc: [
{ label: '', colorKey: 'volumeOscColor', widthKey: 'volumeOscWidth' },
],
vr: [
{ label: '', colorKey: 'vrColor', widthKey: 'vrWidth' },
],
disparity: [
{ label: '초단기', colorKey: 'disparityUltraShortColor', widthKey: 'disparityUltraShortWidth' },
{ label: '단기', colorKey: 'disparityShortColor', widthKey: 'disparityShortWidth' },
{ label: '중기', colorKey: 'disparityMidColor', widthKey: 'disparityMidWidth' },
{ label: '장기', colorKey: 'disparityLongColor', widthKey: 'disparityLongWidth' },
],
};
// 지표별 파라미터 키 매핑
const indicatorParamKeys: Record<string, { key: string; label: string }[]> = {
macdLine: [{ key: 'macdFastPeriod', label: '단기 EMA' }, { key: 'macdSlowPeriod', label: '장기 EMA' }, { key: 'macdSignalPeriod', label: '시그널' }],
rsi: [{ key: 'rsiPeriod', label: '기간' }],
stochasticK: [{ key: 'stochasticKPeriod', label: '기간' }, { key: 'stochasticSlowing', label: '%K' }, { key: 'stochasticDPeriod', label: '%D' }],
cci: [{ key: 'cciPeriod', label: '기간' }, { key: 'cciSignalPeriod', label: '시그널' }],
adx: [{ key: 'adxPeriod', label: '기간' }],
williamsR: [{ key: 'williamsRPeriod', label: '기간' }],
trix: [{ key: 'trixPeriod', label: '기간' }, { key: 'trixSignalPeriod', label: '시그널' }],
newPsychological: [{ key: 'newPsychologicalPeriod', label: '기간' }],
investPsychological: [{ key: 'investPsychologicalPeriod', label: '기간' }],
bwi: [{ key: 'bwiPeriod', label: '기간' }],
obv: [{ key: 'obvPeriod', label: '기간' }],
volumeOsc: [{ key: 'volumeOscShortPeriod', label: '단기' }, { key: 'volumeOscLongPeriod', label: '장기' }],
vr: [{ key: 'vrPeriod', label: '기간' }],
disparity: [
{ key: 'disparityUltraShortPeriod', label: '초단기' },
{ key: 'disparityShortPeriod', label: '단기' },
{ key: 'disparityMidPeriod', label: '중기' },
{ key: 'disparityLongPeriod', label: '장기' },
],
plusDi: [{ key: 'dmiPeriod', label: '기간' }],
minusDi: [{ key: 'dmiPeriod', label: '기간' }],
};
// Sortable 아이템 컴포넌트 - 이동평균선 설정과 동일한 스타일
interface SortableItemProps {
indicator: IndicatorOrder;
isDisabled: boolean;
checked: boolean;
onToggle: () => void;
colors: ChartColorSettings;
updateColors: (c: Partial<ChartColorSettings>) => void;
indicatorSettings: IndicatorSettings;
updateIndicatorSettings: (s: Partial<IndicatorSettings>) => void;
}
const SortableItem: React.FC<SortableItemProps> = ({
indicator,
isDisabled,
checked,
onToggle,
colors,
updateColors,
indicatorSettings,
updateIndicatorSettings,
}) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: indicator.id, disabled: isDisabled || indicator.settingKey === 'candleChart' });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
const canDrag = !isDisabled && indicator.settingKey !== 'candleChart';
const colorConfigs = indicatorColorConfigs[indicator.id] || [];
const paramKeys = indicatorParamKeys[indicator.id];
const hasSettings = indicator.settingKey !== 'candleChart' && indicator.settingKey !== 'volume';
// 첫 번째 색상/굵기 설정
const primaryColor = colorConfigs[0];
// 추가 색상 설정 (시그널 등)
const secondaryColors = colorConfigs.slice(1);
return (
<Paper
ref={setNodeRef}
style={style}
sx={{
mb: 1,
border: isDragging ? '2px solid #1976d2' : checked ? '1px solid' : '1px solid',
borderColor: isDragging ? '#1976d2' : checked ? 'primary.main' : 'divider',
borderRadius: 1,
bgcolor: isDragging ? 'action.hover' : checked ? 'action.selected' : 'background.paper',
boxShadow: isDragging ? 4 : 1,
opacity: checked ? 1 : 0.7,
}}
>
<Box
sx={{
px: 1.5,
py: 1.2,
display: 'flex',
alignItems: 'center',
gap: 1.5,
minHeight: 48,
}}
>
{/* 사용 체크박스 */}
<Box sx={{ width: 40, display: 'flex', justifyContent: 'center' }}>
<Switch
checked={checked}
onChange={onToggle}
disabled={indicator.settingKey === 'candleChart'}
size="small"
/>
</Box>
{/* 드래그 핸들 + 지표명 */}
<Box sx={{ width: 100, display: 'flex', alignItems: 'center', gap: 0.5 }}>
{canDrag && (
<Box
{...attributes}
{...listeners}
sx={{ cursor: 'grab', '&:active': { cursor: 'grabbing' }, display: 'flex' }}
>
<DragIndicator color="action" sx={{ fontSize: 18 }} />
</Box>
)}
<Typography variant="body2" sx={{ fontWeight: checked ? 'bold' : 'normal', fontSize: '13px' }}>
{indicator.label}
</Typography>
</Box>
{/* 파라미터 영역 */}
<Box sx={{ width: 180, display: 'flex', alignItems: 'center', gap: 0.5 }}>
{hasSettings && paramKeys && paramKeys.map((param, idx) => (
<TextField
key={param.key}
type="number"
size="small"
placeholder={param.label}
value={(indicatorSettings as any)[param.key] || ''}
onChange={(e) => updateIndicatorSettings({ [param.key]: Number(e.target.value) })}
onClick={(e) => e.stopPropagation()}
disabled={!checked}
inputProps={{ min: 1, max: 999, style: { fontSize: 12, padding: '6px 8px', textAlign: 'center' } }}
sx={{
width: paramKeys.length > 2 ? 50 : 60,
'& .MuiOutlinedInput-root': { bgcolor: 'background.default' },
}}
/>
))}
</Box>
{/* 색상 영역 */}
<Box sx={{ width: 100, display: 'flex', alignItems: 'center', gap: 1 }}>
{hasSettings && primaryColor && (
<Box sx={{ position: 'relative' }}>
<Box
sx={{
width: 32,
height: 32,
borderRadius: 1,
bgcolor: colors[primaryColor.colorKey] as string,
border: '2px solid',
borderColor: 'divider',
cursor: checked ? 'pointer' : 'default',
opacity: checked ? 1 : 0.5,
}}
/>
<input
type="color"
value={colors[primaryColor.colorKey] as string || '#666666'}
onChange={(e) => updateColors({ [primaryColor.colorKey]: e.target.value })}
onClick={(e) => e.stopPropagation()}
disabled={!checked}
style={{
position: 'absolute', top: 0, left: 0,
width: 32, height: 32, opacity: 0, cursor: 'pointer'
}}
/>
</Box>
)}
{/* 추가 색상 (시그널 등) */}
{hasSettings && secondaryColors.map((cfg) => (
<Box key={cfg.colorKey} sx={{ position: 'relative' }}>
<Box
sx={{
width: 24,
height: 24,
borderRadius: 0.5,
bgcolor: colors[cfg.colorKey] as string,
border: '1px solid',
borderColor: 'divider',
cursor: checked ? 'pointer' : 'default',
opacity: checked ? 1 : 0.5,
}}
/>
<input
type="color"
value={colors[cfg.colorKey] as string || '#666666'}
onChange={(e) => updateColors({ [cfg.colorKey]: e.target.value })}
onClick={(e) => e.stopPropagation()}
disabled={!checked}
style={{
position: 'absolute', top: 0, left: 0,
width: 24, height: 24, opacity: 0, cursor: 'pointer'
}}
/>
</Box>
))}
</Box>
{/* 선 굵기 영역 */}
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
{hasSettings && primaryColor?.widthKey && (
<>
<Slider
value={colors[primaryColor.widthKey] as number || 1}
onChange={(_, value) => updateColors({ [primaryColor.widthKey!]: value as number })}
onClick={(e) => e.stopPropagation()}
disabled={!checked}
min={0.5}
max={5}
step={0.5}
sx={{ flex: 1, maxWidth: 150 }}
size="small"
/>
<Typography variant="caption" sx={{ minWidth: 30, fontSize: '11px', color: 'text.secondary' }}>
{colors[primaryColor.widthKey]}px
</Typography>
{/* 선 미리보기 */}
<Box
sx={{
width: 40,
height: Math.max((colors[primaryColor.widthKey] as number || 1) * 2, 2),
bgcolor: checked ? colors[primaryColor.colorKey] as string : 'grey.400',
borderRadius: 0.5,
}}
/>
</>
)}
</Box>
</Box>
</Paper>
);
};
const IndicatorSettingsDialog: React.FC<IndicatorSettingsDialogProps> = ({ open, onClose }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const { settings, order, updateSettings, updateOrder, resetSettings, saveSettings } = useIndicatorVisibility();
const { colors, updateColors } = useChartColors();
const { indicatorSettings, updateIndicatorSettings } = useIndicatorSettings();
const { isAuthenticated } = useAuth();
const [tempSettings, setTempSettings] = useState<IndicatorVisibilitySettings>(settings);
const [tempOrder, setTempOrder] = useState<IndicatorOrder[]>(order);
const [activeId, setActiveId] = useState<string | null>(null);
// 드래그 센서 설정 - 즉시 드래그 시작 가능하도록 설정
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 5, // 5px 이동 후 드래그 시작
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 100, // 100ms 터치 후 드래그 시작
tolerance: 5,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// 다이얼로그가 열릴 때마다 현재 설정으로 초기화
React.useEffect(() => {
if (open) {
setTempSettings(settings);
setTempOrder(order);
}
}, [open, settings, order]);
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string);
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
setActiveId(null);
if (over && active.id !== over.id) {
setTempOrder((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
const handleToggle = (key: keyof IndicatorVisibilitySettings) => {
// candleChart는 변경 불가
if (key === 'candleChart') return;
setTempSettings(prev => ({ ...prev, [key]: !prev[key] }));
};
const handleSave = async () => {
updateSettings(tempSettings);
updateOrder(tempOrder);
await saveSettings(isAuthenticated);
onClose();
};
const handleReset = () => {
if (window.confirm('모든 보조지표 설정을 기본값으로 초기화하시겠습니까?')) {
resetSettings();
onClose();
}
};
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="md"
fullWidth
fullScreen={isMobile}
hideBackdrop={true}
disableScrollLock={true}
PaperComponent={DraggablePaper}
sx={{
pointerEvents: 'none',
'& .MuiDialog-container': {
pointerEvents: 'none',
alignItems: 'flex-start',
},
}}
PaperProps={{
sx: {
minHeight: isMobile ? '100%' : '60vh',
maxHeight: isMobile ? '100%' : '90vh',
width: isMobile ? '100%' : 750,
pointerEvents: 'auto',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
border: '2px solid rgba(80, 100, 160, 0.5)',
}
}}
>
<DialogTitle sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
bgcolor: '#1976d2',
color: '#fff',
py: 1.2,
}}>
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px', color: '#fff' }}>
📊
</Typography>
<IconButton onClick={onClose} size="small" sx={{ color: '#fff' }}>
<Close />
</IconButton>
</DialogTitle>
<DialogContent dividers>
{/* 헤더 행 - 이동평균선 설정과 동일한 스타일 */}
<Paper sx={{ p: 1.5, mb: 2, bgcolor: 'action.hover' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{ width: 40, textAlign: 'center' }}>
<Typography variant="caption" fontWeight="bold" color="text.secondary"></Typography>
</Box>
<Box sx={{ width: 100 }}>
<Typography variant="caption" fontWeight="bold" color="text.secondary"></Typography>
</Box>
<Box sx={{ width: 180 }}>
<Typography variant="caption" fontWeight="bold" color="text.secondary"></Typography>
</Box>
<Box sx={{ width: 100 }}>
<Typography variant="caption" fontWeight="bold" color="text.secondary"></Typography>
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="caption" fontWeight="bold" color="text.secondary"> </Typography>
</Box>
</Box>
</Paper>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext
items={tempOrder.map(item => item.id)}
strategy={verticalListSortingStrategy}
>
{tempOrder.map((indicator) => {
const value = tempSettings[indicator.settingKey];
const isChecked = typeof value === 'boolean' ? value : false;
return (
<SortableItem
key={indicator.id}
indicator={indicator}
isDisabled={false}
checked={isChecked}
onToggle={() => handleToggle(indicator.settingKey)}
colors={colors}
updateColors={updateColors}
indicatorSettings={indicatorSettings}
updateIndicatorSettings={updateIndicatorSettings}
/>
);
})}
</SortableContext>
<DragOverlay>
{activeId ? (
<Paper
sx={{
p: 1.5,
display: 'flex',
alignItems: 'center',
border: '2px solid #1976d2',
borderRadius: 1,
bgcolor: 'background.paper',
boxShadow: 6,
opacity: 0.9,
}}
>
<DragIndicator color="primary" sx={{ mr: 1.5 }} />
<Typography variant="body1">
{tempOrder.find(item => item.id === activeId)?.label}
</Typography>
</Paper>
) : null}
</DragOverlay>
</DndContext>
</DialogContent>
<DialogActions sx={{ p: 2 }}>
<Button onClick={handleReset} color="error" variant="outlined">
</Button>
<Box sx={{ flex: 1 }} />
<Button onClick={onClose} variant="outlined">
</Button>
<Button onClick={handleSave} variant="contained" color="primary">
</Button>
</DialogActions>
</Dialog>
);
};
export default IndicatorSettingsDialog;