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 = ({ objects, chartWidth, chartHeight, totalChartHeight, timeScale, candleData, timeToX: timeToXProp, // ✅ prop으로 받은 함수 onClick, onDoubleClick, onDragStart, isDrawing = false, drawingStartTime = null, }) => { const svgRef = useRef(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 ( {/* 드로잉 중일 때 기준선 표시 */} {isDrawing && drawingStartTime !== null && (() => { const x = timeToX(drawingStartTime); if (x === null) return null; return ( 기준점 ); })()} {/* 완성된 피보나치 타임존 표시 */} {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 ( {/* ✅ 기준선 (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 ( {/* 투명한 클릭 영역 (드래그하기 쉽도록 넓게) */} { 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); } }} /> {/* 기준선 */} {/* 레이블 */} 0 ); })()} {/* 피보나치 레벨 선들 */} {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 ( {/* 투명한 클릭 영역 (드래그하기 쉽도록 넓게) */} { 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); } }} /> {/* 실제 수직선 */} {/* 레이블 */} {level.value} ); })} ); })} ); }; export default FibonacciTimezoneOverlay;