goldenChat base source add
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { FibonacciTimezoneSettings, FibonacciLevel, FibonacciBaseline, defaultBaseline } from './FibonacciTimezoneDialog';
|
||||
|
||||
export interface FibonacciTimezoneObject {
|
||||
id: string;
|
||||
startTime: number; // Unix timestamp (seconds) - 기준점
|
||||
endTime: number; // Unix timestamp (seconds) - 두 번째 클릭 지점 (방향 결정용)
|
||||
baseInterval: number; // 방향 (1 = 오른쪽, -1 = 왼쪽)
|
||||
settings: FibonacciTimezoneSettings;
|
||||
}
|
||||
|
||||
interface FibonacciTimezoneOverlayProps {
|
||||
objects: FibonacciTimezoneObject[];
|
||||
chartWidth: number;
|
||||
chartHeight: number; // 캔들차트 높이
|
||||
totalChartHeight?: number; // 캔들차트 + 모든 보조지표 차트 높이 (보조지표까지 보기 옵션용)
|
||||
timeScale: {
|
||||
from: number;
|
||||
to: number;
|
||||
};
|
||||
candleData: Array<{ time: string }>;
|
||||
timeToX: (time: number) => number | null; // ✅ 차트의 timeToCoordinate 함수 전달 (null 가능)
|
||||
onClick?: (objectId: string, event: React.MouseEvent) => void; // ✅ 클릭 핸들러
|
||||
onDoubleClick?: (objectId: string) => void;
|
||||
onDragStart?: (objectId: string, startX: number, startTime: number) => void;
|
||||
isDrawing?: boolean; // 드로잉 중인지 여부
|
||||
drawingStartTime?: number | null; // 드로잉 중 시작 시간
|
||||
}
|
||||
|
||||
const FibonacciTimezoneOverlay: React.FC<FibonacciTimezoneOverlayProps> = ({
|
||||
objects,
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
totalChartHeight,
|
||||
timeScale,
|
||||
candleData,
|
||||
timeToX: timeToXProp, // ✅ prop으로 받은 함수
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
onDragStart,
|
||||
isDrawing = false,
|
||||
drawingStartTime = null,
|
||||
}) => {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
|
||||
// 시간을 캔들 인덱스로 변환 (가장 가까운 캔들 찾기)
|
||||
const timeToIndex = (time: number): number => {
|
||||
if (candleData.length === 0) return -1;
|
||||
|
||||
let nearestIndex = 0;
|
||||
let minDiff = Infinity;
|
||||
|
||||
for (let i = 0; i < candleData.length; i++) {
|
||||
const candleTime = new Date(candleData[i].time).getTime() / 1000;
|
||||
const diff = Math.abs(candleTime - time);
|
||||
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
nearestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return nearestIndex;
|
||||
};
|
||||
|
||||
// 캔들 인덱스를 시간으로 변환
|
||||
const indexToTime = (index: number): number => {
|
||||
if (index < 0 || index >= candleData.length) return 0;
|
||||
return new Date(candleData[index].time).getTime() / 1000;
|
||||
};
|
||||
|
||||
// ✅ prop으로 받은 timeToX 함수 사용
|
||||
const timeToX = timeToXProp;
|
||||
|
||||
// 선 스타일을 SVG stroke-dasharray로 변환
|
||||
const getStrokeDasharray = (style: string): string => {
|
||||
switch (style) {
|
||||
case 'dashed':
|
||||
return '5,5';
|
||||
case 'dotted':
|
||||
return '2,2';
|
||||
default:
|
||||
return '0';
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoubleClick = (objectId: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (onDoubleClick) {
|
||||
onDoubleClick(objectId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown = (objectId: string, e: React.MouseEvent, startTime: number) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault(); // 기본 드래그 동작 방지
|
||||
console.log(`🖱️ [오버레이] mouseDown - objectId: ${objectId}, startTime: ${startTime}`);
|
||||
if (onDragStart) {
|
||||
const startX = e.clientX;
|
||||
onDragStart(objectId, startX, startTime);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 최대 높이 계산 (보조지표까지 보기가 활성화된 피보나치가 있을 경우)
|
||||
const maxHeight = objects.some(obj => obj.settings.showAcrossIndicators) && totalChartHeight
|
||||
? totalChartHeight
|
||||
: chartHeight;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: maxHeight,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 50,
|
||||
overflow: 'visible', // ✅ 오버플로우 허용
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width="100%"
|
||||
height={maxHeight}
|
||||
style={{ position: 'absolute', top: 0, left: 0 }}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
{/* 드로잉 중일 때 기준선 표시 */}
|
||||
{isDrawing && drawingStartTime !== null && (() => {
|
||||
const x = timeToX(drawingStartTime);
|
||||
if (x === null) return null;
|
||||
return (
|
||||
<g>
|
||||
<line
|
||||
x1={x}
|
||||
y1={0}
|
||||
x2={x}
|
||||
y2={chartHeight}
|
||||
stroke="rgba(255, 255, 0, 0.8)" // 노란색 기준선
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5,5"
|
||||
/>
|
||||
<text
|
||||
x={x + 5}
|
||||
y={20}
|
||||
fill="rgba(255, 255, 0, 0.8)"
|
||||
fontSize="12px"
|
||||
fontWeight="bold"
|
||||
>
|
||||
기준점
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 완성된 피보나치 타임존 표시 */}
|
||||
{objects
|
||||
.filter((obj) => obj.id !== 'temp-drawing') // 임시 드로잉 제외
|
||||
.map((obj) => {
|
||||
const startIndex = timeToIndex(obj.startTime);
|
||||
const endIndex = timeToIndex(obj.endTime);
|
||||
|
||||
if (startIndex === -1 || endIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// baseInterval은 방향 (1 = 오른쪽, -1 = 왼쪽)
|
||||
const direction = obj.baseInterval;
|
||||
|
||||
const startX = timeToX(obj.startTime);
|
||||
|
||||
// ✅ 보조지표까지 보기 옵션에 따라 높이 결정
|
||||
const effectiveHeight = obj.settings.showAcrossIndicators && totalChartHeight
|
||||
? totalChartHeight
|
||||
: chartHeight;
|
||||
|
||||
// 디버깅: 설정 확인
|
||||
console.log(`🎨 [피보나치 렌더링] ID: ${obj.id}, 활성화된 레벨:`, obj.settings.levels.filter(l => l.enabled).map(l => l.value), `showAcrossIndicators: ${obj.settings.showAcrossIndicators}, effectiveHeight: ${effectiveHeight}`);
|
||||
|
||||
return (
|
||||
<g
|
||||
key={obj.id}
|
||||
>
|
||||
{/* ✅ 기준선 (baseline) 렌더링 */}
|
||||
{(() => {
|
||||
// ✅ baseline이 없으면 기본값 사용 (기존 저장된 데이터 호환)
|
||||
const baseline = obj.settings.baseline || defaultBaseline;
|
||||
console.log(`🎨 [피보나치 기준선 렌더링] ID: ${obj.id}, baseline:`, baseline);
|
||||
const x = timeToX(obj.startTime);
|
||||
if (x === null) return null;
|
||||
|
||||
return (
|
||||
<g key={`${obj.id}-baseline`}>
|
||||
{/* 투명한 클릭 영역 (드래그하기 쉽도록 넓게) */}
|
||||
<line
|
||||
x1={x}
|
||||
y1={0}
|
||||
x2={x}
|
||||
y2={effectiveHeight}
|
||||
stroke="transparent"
|
||||
strokeWidth={40}
|
||||
style={{ pointerEvents: 'auto', cursor: 'move' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onClick) {
|
||||
onClick(obj.id, e);
|
||||
}
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (onDragStart) {
|
||||
const startX = e.clientX;
|
||||
onDragStart(obj.id, startX, obj.startTime);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onDoubleClick) {
|
||||
onDoubleClick(obj.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* 기준선 */}
|
||||
<line
|
||||
x1={x}
|
||||
y1={0}
|
||||
x2={x}
|
||||
y2={effectiveHeight}
|
||||
stroke={baseline.color}
|
||||
strokeWidth={baseline.lineWidth}
|
||||
strokeDasharray={getStrokeDasharray(baseline.lineStyle)}
|
||||
opacity={0.8}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
{/* 레이블 */}
|
||||
<text
|
||||
x={x + 5}
|
||||
y={20}
|
||||
fill={baseline.color}
|
||||
fontSize="11px"
|
||||
fontWeight="bold"
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
0
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 피보나치 레벨 선들 */}
|
||||
{obj.settings.levels
|
||||
.filter((level) => level.enabled)
|
||||
.map((level, index) => {
|
||||
|
||||
// 캔들 인덱스 계산: 기준점 + (레벨 값 * 방향)
|
||||
// 레벨 값이 직접 캔들 간격이 됨 (예: 1, 2, 3, 5, 8...)
|
||||
const targetIndex = startIndex + (level.value * direction);
|
||||
|
||||
// ✅ 시간 계산 (범위를 벗어나도 추정 가능)
|
||||
let time: number;
|
||||
if (targetIndex >= 0 && targetIndex < candleData.length) {
|
||||
// 범위 내: 정확한 캔들 시간 사용
|
||||
time = indexToTime(targetIndex);
|
||||
console.log(`📍 [오버레이] 레벨 ${level.value}: 범위 내 정확한 시간 사용, targetIndex=${targetIndex}, time=${time}`);
|
||||
} else if (candleData.length >= 2) {
|
||||
// 범위 외: 캔들 간격을 사용하여 시간 추정
|
||||
const lastIndex = candleData.length - 1;
|
||||
const firstTime = new Date(candleData[0].time).getTime() / 1000;
|
||||
const lastTime = new Date(candleData[lastIndex].time).getTime() / 1000;
|
||||
const candleInterval = (lastTime - firstTime) / lastIndex; // 평균 캔들 간격
|
||||
|
||||
const startTime = new Date(candleData[Math.max(0, Math.min(startIndex, lastIndex))].time).getTime() / 1000;
|
||||
time = startTime + (level.value * direction * candleInterval);
|
||||
console.log(`📍 [오버레이] 레벨 ${level.value}: 범위 외 시간 추정, targetIndex=${targetIndex}, time=${time}, interval=${candleInterval}`);
|
||||
} else {
|
||||
// 캔들 데이터가 부족하면 건너뛰기
|
||||
console.log(`❌ [오버레이] 레벨 ${level.value}: 캔들 데이터 부족`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const x = timeToX(time);
|
||||
console.log(`📏 [오버레이] 레벨 ${level.value}: time=${time}, x=${x}, chartWidth=${chartWidth}`);
|
||||
|
||||
// ✅ x가 null이면 건너뛰기 (범위 체크는 매우 관대하게)
|
||||
if (x === null) {
|
||||
console.log(`❌ [오버레이] 레벨 ${level.value}: x가 null`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// ✅ 매우 먼 거리만 제외 (차트의 10배 범위까지 허용)
|
||||
const maxDistance = chartWidth * 10;
|
||||
if (x < -maxDistance || x > chartWidth + maxDistance) {
|
||||
console.log(`❌ [오버레이] 레벨 ${level.value}: x가 너무 멀리 벗어남 (x=${x}, 허용범위: ${-maxDistance} ~ ${chartWidth + maxDistance})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`✅ [오버레이] 레벨 ${level.value}: 렌더링 (x=${x})`);
|
||||
|
||||
|
||||
return (
|
||||
<g key={`${obj.id}-level-${index}`}>
|
||||
{/* 투명한 클릭 영역 (드래그하기 쉽도록 넓게) */}
|
||||
<line
|
||||
x1={x}
|
||||
y1={0}
|
||||
x2={x}
|
||||
y2={effectiveHeight}
|
||||
stroke="transparent"
|
||||
strokeWidth={40} // 넓은 클릭 영역
|
||||
style={{ pointerEvents: 'auto', cursor: 'move' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onClick) {
|
||||
onClick(obj.id, e);
|
||||
}
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log(`🖱️ [투명 영역] mouseDown - objectId: ${obj.id}, level: ${level.value}, x: ${x}`);
|
||||
if (onDragStart) {
|
||||
const startX = e.clientX;
|
||||
onDragStart(obj.id, startX, obj.startTime);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log(`🖱️ [투명 영역] doubleClick - objectId: ${obj.id}, level: ${level.value}`);
|
||||
if (onDoubleClick) {
|
||||
onDoubleClick(obj.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* 실제 수직선 */}
|
||||
<line
|
||||
x1={x}
|
||||
y1={0}
|
||||
x2={x}
|
||||
y2={effectiveHeight}
|
||||
stroke={level.color}
|
||||
strokeWidth={level.lineWidth}
|
||||
strokeDasharray={getStrokeDasharray(level.lineStyle)}
|
||||
opacity={0.8}
|
||||
style={{ pointerEvents: 'none' }} // 투명 영역만 이벤트 받도록
|
||||
/>
|
||||
{/* 레이블 */}
|
||||
<text
|
||||
x={x + 5}
|
||||
y={20}
|
||||
fill={level.color}
|
||||
fontSize="11px"
|
||||
fontWeight="bold"
|
||||
style={{ pointerEvents: 'none' }} // 투명 영역만 이벤트 받도록
|
||||
>
|
||||
{level.value}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default FibonacciTimezoneOverlay;
|
||||
|
||||
Reference in New Issue
Block a user