370 lines
12 KiB
TypeScript
370 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
Button,
|
|
Box,
|
|
Checkbox,
|
|
IconButton,
|
|
Typography,
|
|
Divider,
|
|
Select,
|
|
MenuItem,
|
|
FormControl,
|
|
Slider,
|
|
Popover,
|
|
TextField,
|
|
} from '@mui/material';
|
|
import { Close as CloseIcon, Edit as EditIcon } from '@mui/icons-material';
|
|
import DraggablePaper from './DraggablePaper';
|
|
|
|
export interface VerticalLineSettings {
|
|
showLine: boolean; // 라인
|
|
extended: boolean; // 확장 (보조지표까지)
|
|
showLabel: boolean; // 타입 라벨 (날짜)
|
|
customText?: string; // 커스텀 텍스트 (상단 표시)
|
|
color: string;
|
|
lineStyle: 'solid' | 'dashed' | 'dotted';
|
|
lineWidth: number;
|
|
opacity?: number; // 투명도 (0-100)
|
|
}
|
|
|
|
interface VerticalLineDialogProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
settings: VerticalLineSettings;
|
|
onSave: (settings: VerticalLineSettings) => void;
|
|
onDelete?: () => void;
|
|
}
|
|
|
|
export const defaultVerticalLineSettings: VerticalLineSettings = {
|
|
showLine: true,
|
|
extended: true, // ✅ 기본값: 보조지표까지 확장
|
|
showLabel: true,
|
|
customText: '',
|
|
color: '#2962ff',
|
|
lineStyle: 'solid',
|
|
lineWidth: 2,
|
|
opacity: 100,
|
|
};
|
|
|
|
const VerticalLineDialog: React.FC<VerticalLineDialogProps> = ({
|
|
open,
|
|
onClose,
|
|
settings,
|
|
onSave,
|
|
onDelete,
|
|
}) => {
|
|
const [localSettings, setLocalSettings] = useState<VerticalLineSettings>(settings);
|
|
const [colorPickerAnchor, setColorPickerAnchor] = useState<HTMLElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
setLocalSettings(settings);
|
|
}, [settings]);
|
|
|
|
// 색상 팔레트
|
|
const colorPalette = [
|
|
['#000000', '#434651', '#666666', '#999999', '#B2B5BE', '#D1D4DC', '#E1E3EB', '#F0F3FA'],
|
|
['#F23645', '#FF9800', '#FDD835', '#26A69A', '#00BCD4', '#2196F3', '#3F51B5', '#9C27B0'],
|
|
['#880E4F', '#B71C1C', '#E65100', '#F57F17', '#827717', '#33691E', '#1B5E20', '#004D40'],
|
|
['#FF5252', '#FF6E40', '#FFAB40', '#FFD740', '#AEEA00', '#69F0AE', '#40C4FF', '#448AFF'],
|
|
['#FF80AB', '#FF4081', '#F50057', '#C51162', '#880E4F', '#4A148C', '#311B92', '#1A237E'],
|
|
['#EA80FC', '#E040FB', '#D500F9', '#AA00FF', '#6200EA', '#304FFE', '#2962FF', '#0091EA'],
|
|
['#8C9EFF', '#536DFE', '#3D5AFE', '#1976D2', '#0277BD', '#006064', '#004D40', '#1B5E20'],
|
|
];
|
|
|
|
const handleColorPickerOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
setColorPickerAnchor(event.currentTarget);
|
|
};
|
|
|
|
const handleColorPickerClose = () => {
|
|
setColorPickerAnchor(null);
|
|
};
|
|
|
|
const handleColorSelect = (color: string) => {
|
|
setLocalSettings({ ...localSettings, color });
|
|
};
|
|
|
|
const handleSave = () => {
|
|
onSave(localSettings);
|
|
onClose();
|
|
};
|
|
|
|
const handleReset = () => {
|
|
setLocalSettings(defaultVerticalLineSettings);
|
|
};
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onClose={onClose}
|
|
maxWidth="xs"
|
|
fullWidth
|
|
hideBackdrop={true}
|
|
disableScrollLock={true}
|
|
PaperComponent={DraggablePaper}
|
|
sx={{
|
|
pointerEvents: 'none',
|
|
'& .MuiDialog-container': {
|
|
pointerEvents: 'none',
|
|
alignItems: 'flex-start',
|
|
},
|
|
}}
|
|
PaperProps={{
|
|
sx: {
|
|
bgcolor: 'background.paper',
|
|
borderRadius: 2,
|
|
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' }}>
|
|
<CloseIcon />
|
|
</IconButton>
|
|
</DialogTitle>
|
|
|
|
<Divider />
|
|
|
|
<DialogContent sx={{ pt: 3, pb: 2 }}>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
{/* 라인 체크박스 */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<Checkbox
|
|
checked={localSettings.showLine}
|
|
onChange={(e) => setLocalSettings({ ...localSettings, showLine: e.target.checked })}
|
|
size="small"
|
|
/>
|
|
<Typography variant="body2">라인</Typography>
|
|
|
|
{/* 색상 선택 */}
|
|
{localSettings.showLine && (
|
|
<Box
|
|
onClick={handleColorPickerOpen}
|
|
sx={{
|
|
ml: 'auto',
|
|
width: 60,
|
|
height: 30,
|
|
bgcolor: localSettings.color,
|
|
borderRadius: 1,
|
|
border: '2px solid',
|
|
borderColor: 'primary.main',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
'&:hover': {
|
|
opacity: 0.8,
|
|
},
|
|
}}
|
|
>
|
|
<EditIcon sx={{ fontSize: 16, color: 'white', textShadow: '0 0 2px black' }} />
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{/* 선 스타일 */}
|
|
{localSettings.showLine && (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, pl: 4 }}>
|
|
<Typography variant="caption" sx={{ minWidth: 60 }}>스타일:</Typography>
|
|
<FormControl size="small" sx={{ minWidth: 120 }}>
|
|
<Select
|
|
value={localSettings.lineStyle}
|
|
onChange={(e) => setLocalSettings({ ...localSettings, lineStyle: e.target.value as any })}
|
|
>
|
|
<MenuItem value="solid">━━━</MenuItem>
|
|
<MenuItem value="dashed">- - -</MenuItem>
|
|
<MenuItem value="dotted">· · ·</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
|
|
<Typography variant="caption" sx={{ minWidth: 40, ml: 2 }}>두께:</Typography>
|
|
<FormControl size="small" sx={{ minWidth: 80 }}>
|
|
<Select
|
|
value={localSettings.lineWidth}
|
|
onChange={(e) => setLocalSettings({ ...localSettings, lineWidth: Number(e.target.value) })}
|
|
>
|
|
<MenuItem value={1}>1</MenuItem>
|
|
<MenuItem value={2}>2</MenuItem>
|
|
<MenuItem value={3}>3</MenuItem>
|
|
<MenuItem value={4}>4</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
)}
|
|
|
|
<Divider />
|
|
|
|
{/* 확장 체크박스 */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<Checkbox
|
|
checked={localSettings.extended}
|
|
onChange={(e) => setLocalSettings({ ...localSettings, extended: e.target.checked })}
|
|
size="small"
|
|
/>
|
|
<Typography variant="body2">확장</Typography>
|
|
<Typography variant="caption" color="text.secondary" sx={{ ml: 'auto' }}>
|
|
{localSettings.extended ? '보조지표까지 표시' : '캔들차트만 표시'}
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* 타입 라벨 체크박스 */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<Checkbox
|
|
checked={localSettings.showLabel}
|
|
onChange={(e) => setLocalSettings({ ...localSettings, showLabel: e.target.checked })}
|
|
size="small"
|
|
/>
|
|
<Typography variant="body2">타입 라벨</Typography>
|
|
<Typography variant="caption" color="text.secondary" sx={{ ml: 'auto' }}>
|
|
{localSettings.showLabel ? '날짜 표시' : '날짜 숨김'}
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Divider />
|
|
|
|
{/* 커스텀 텍스트 입력 */}
|
|
<Box>
|
|
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
|
|
수직선 상단 텍스트
|
|
</Typography>
|
|
<TextField
|
|
fullWidth
|
|
size="small"
|
|
placeholder="텍스트를 입력하세요"
|
|
value={localSettings.customText || ''}
|
|
onChange={(e) => setLocalSettings({ ...localSettings, customText: e.target.value })}
|
|
helperText="입력한 텍스트가 수직선 상단에 표시됩니다"
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* 색상 팔레트 팝오버 */}
|
|
<Popover
|
|
open={Boolean(colorPickerAnchor)}
|
|
anchorEl={colorPickerAnchor}
|
|
onClose={handleColorPickerClose}
|
|
anchorOrigin={{
|
|
vertical: 'bottom',
|
|
horizontal: 'left',
|
|
}}
|
|
>
|
|
<Box sx={{ p: 2, width: 240 }}>
|
|
{/* 색상 팔레트 */}
|
|
{colorPalette.map((row, rowIndex) => (
|
|
<Box key={rowIndex} sx={{ display: 'flex', gap: 0.5, mb: 0.5 }}>
|
|
{row.map((color, colIndex) => (
|
|
<Box
|
|
key={colIndex}
|
|
onClick={() => {
|
|
handleColorSelect(color);
|
|
handleColorPickerClose();
|
|
}}
|
|
sx={{
|
|
width: 24,
|
|
height: 24,
|
|
bgcolor: color,
|
|
cursor: 'pointer',
|
|
border: '1px solid rgba(0,0,0,0.1)',
|
|
'&:hover': {
|
|
border: '2px solid white',
|
|
boxShadow: '0 0 4px rgba(0,0,0,0.3)',
|
|
},
|
|
}}
|
|
/>
|
|
))}
|
|
</Box>
|
|
))}
|
|
|
|
{/* 투명도 슬라이더 */}
|
|
<Box sx={{ mt: 2 }}>
|
|
<Typography variant="caption" gutterBottom>
|
|
투명도
|
|
</Typography>
|
|
<Slider
|
|
value={localSettings.opacity || 100}
|
|
onChange={(e, value) => {
|
|
setLocalSettings({ ...localSettings, opacity: value as number });
|
|
}}
|
|
min={0}
|
|
max={100}
|
|
valueLabelDisplay="auto"
|
|
valueLabelFormat={(value) => `${value}%`}
|
|
sx={{ width: '100%' }}
|
|
/>
|
|
</Box>
|
|
|
|
{/* 커스텀 색상 입력 */}
|
|
<Box sx={{ mt: 2, display: 'flex', gap: 1, alignItems: 'center' }}>
|
|
<Typography variant="caption">색상 코드:</Typography>
|
|
<input
|
|
type="text"
|
|
placeholder="#000000"
|
|
value={localSettings.color}
|
|
onChange={(e) => {
|
|
const color = e.target.value;
|
|
if (/^#[0-9A-F]{6}$/i.test(color)) {
|
|
handleColorSelect(color);
|
|
}
|
|
}}
|
|
style={{
|
|
flex: 1,
|
|
padding: '4px 8px',
|
|
border: '1px solid #ccc',
|
|
borderRadius: '4px',
|
|
fontSize: '12px',
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
</Popover>
|
|
</DialogContent>
|
|
|
|
<Divider />
|
|
|
|
<DialogActions sx={{ p: 2, gap: 1 }}>
|
|
{onDelete && (
|
|
<Button
|
|
onClick={() => {
|
|
if (window.confirm('이 수직선을 삭제하시겠습니까?')) {
|
|
onDelete();
|
|
onClose();
|
|
}
|
|
}}
|
|
variant="outlined"
|
|
color="error"
|
|
>
|
|
삭제
|
|
</Button>
|
|
)}
|
|
<Button onClick={handleReset} variant="outlined" color="secondary">
|
|
초기화
|
|
</Button>
|
|
<Box sx={{ flex: 1 }} />
|
|
<Button onClick={onClose} variant="outlined">
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleSave} variant="contained" color="primary">
|
|
확인
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default VerticalLineDialog;
|