dipping-charts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -0
  3. package/dist/__tests__/FullFeaturedChart.test.d.ts +2 -0
  4. package/dist/__tests__/FullFeaturedChart.test.d.ts.map +1 -0
  5. package/dist/__tests__/indicators-accuracy.test.d.ts +2 -0
  6. package/dist/__tests__/indicators-accuracy.test.d.ts.map +1 -0
  7. package/dist/__tests__/indicators.test.d.ts +2 -0
  8. package/dist/__tests__/indicators.test.d.ts.map +1 -0
  9. package/dist/__tests__/setup.d.ts +1 -0
  10. package/dist/__tests__/setup.d.ts.map +1 -0
  11. package/dist/__tests__/validateCandle.test.d.ts +2 -0
  12. package/dist/__tests__/validateCandle.test.d.ts.map +1 -0
  13. package/dist/chart/index.d.ts +2 -0
  14. package/dist/chart/index.js +5 -0
  15. package/dist/chart/index.js.map +1 -0
  16. package/dist/components/TradingChart.d.ts +24 -0
  17. package/dist/components/TradingChart.d.ts.map +1 -0
  18. package/dist/components/TradingChart.js +100 -0
  19. package/dist/components/TradingChart.js.map +1 -0
  20. package/dist/components/index.d.ts +3 -0
  21. package/dist/components/index.d.ts.map +1 -0
  22. package/dist/dipping-charts.css +1 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +28 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/indicators/atr.d.ts +15 -0
  28. package/dist/indicators/atr.d.ts.map +1 -0
  29. package/dist/indicators/atr.js +30 -0
  30. package/dist/indicators/atr.js.map +1 -0
  31. package/dist/indicators/bollingerBands.d.ts +11 -0
  32. package/dist/indicators/bollingerBands.d.ts.map +1 -0
  33. package/dist/indicators/bollingerBands.js +39 -0
  34. package/dist/indicators/bollingerBands.js.map +1 -0
  35. package/dist/indicators/currencyStrength.d.ts +43 -0
  36. package/dist/indicators/currencyStrength.d.ts.map +1 -0
  37. package/dist/indicators/currencyStrength.js +53 -0
  38. package/dist/indicators/currencyStrength.js.map +1 -0
  39. package/dist/indicators/ema.d.ts +11 -0
  40. package/dist/indicators/ema.d.ts.map +1 -0
  41. package/dist/indicators/ema.js +24 -0
  42. package/dist/indicators/ema.js.map +1 -0
  43. package/dist/indicators/index.d.ts +19 -0
  44. package/dist/indicators/index.d.ts.map +1 -0
  45. package/dist/indicators/index.js +23 -0
  46. package/dist/indicators/index.js.map +1 -0
  47. package/dist/indicators/macd.d.ts +11 -0
  48. package/dist/indicators/macd.d.ts.map +1 -0
  49. package/dist/indicators/macd.js +52 -0
  50. package/dist/indicators/macd.js.map +1 -0
  51. package/dist/indicators/rsi.d.ts +11 -0
  52. package/dist/indicators/rsi.d.ts.map +1 -0
  53. package/dist/indicators/rsi.js +29 -0
  54. package/dist/indicators/rsi.js.map +1 -0
  55. package/dist/indicators/sma.d.ts +13 -0
  56. package/dist/indicators/sma.d.ts.map +1 -0
  57. package/dist/indicators/sma.js +22 -0
  58. package/dist/indicators/sma.js.map +1 -0
  59. package/dist/indicators/stochastic.d.ts +15 -0
  60. package/dist/indicators/stochastic.d.ts.map +1 -0
  61. package/dist/indicators/stochastic.js +34 -0
  62. package/dist/indicators/stochastic.js.map +1 -0
  63. package/dist/indicators/types.d.ts +102 -0
  64. package/dist/indicators/types.d.ts.map +1 -0
  65. package/dist/indicators/vwap.d.ts +14 -0
  66. package/dist/indicators/vwap.d.ts.map +1 -0
  67. package/dist/indicators/vwap.js +17 -0
  68. package/dist/indicators/vwap.js.map +1 -0
  69. package/dist/indicators/williamsR.d.ts +17 -0
  70. package/dist/indicators/williamsR.d.ts.map +1 -0
  71. package/dist/indicators/williamsR.js +19 -0
  72. package/dist/indicators/williamsR.js.map +1 -0
  73. package/dist/react/FullFeaturedChart.d.ts +3 -0
  74. package/dist/react/FullFeaturedChart.d.ts.map +1 -0
  75. package/dist/react/FullFeaturedChart.js +640 -0
  76. package/dist/react/FullFeaturedChart.js.map +1 -0
  77. package/dist/react/components/IndicatorSettings.d.ts +20 -0
  78. package/dist/react/components/IndicatorSettings.d.ts.map +1 -0
  79. package/dist/react/components/IndicatorSettings.js +748 -0
  80. package/dist/react/components/IndicatorSettings.js.map +1 -0
  81. package/dist/react/hooks/useChart.d.ts +15 -0
  82. package/dist/react/hooks/useChart.d.ts.map +1 -0
  83. package/dist/react/hooks/useChart.js +155 -0
  84. package/dist/react/hooks/useChart.js.map +1 -0
  85. package/dist/react/hooks/useIndicators.d.ts +10 -0
  86. package/dist/react/hooks/useIndicators.d.ts.map +1 -0
  87. package/dist/react/hooks/useIndicators.js +264 -0
  88. package/dist/react/hooks/useIndicators.js.map +1 -0
  89. package/dist/react/hooks/useLineTools.d.ts +26 -0
  90. package/dist/react/hooks/useLineTools.d.ts.map +1 -0
  91. package/dist/react/hooks/useLineTools.js +189 -0
  92. package/dist/react/hooks/useLineTools.js.map +1 -0
  93. package/dist/react/hooks/useShiftSnap.d.ts +12 -0
  94. package/dist/react/hooks/useShiftSnap.d.ts.map +1 -0
  95. package/dist/react/hooks/useShiftSnap.js +54 -0
  96. package/dist/react/hooks/useShiftSnap.js.map +1 -0
  97. package/dist/react/index.d.ts +14 -0
  98. package/dist/react/index.d.ts.map +1 -0
  99. package/dist/react/index.js +18 -0
  100. package/dist/react/index.js.map +1 -0
  101. package/dist/react/loadLightweightCharts.d.ts +18 -0
  102. package/dist/react/loadLightweightCharts.d.ts.map +1 -0
  103. package/dist/react/loadLightweightCharts.js +32 -0
  104. package/dist/react/loadLightweightCharts.js.map +1 -0
  105. package/dist/react/locale.d.ts +79 -0
  106. package/dist/react/locale.d.ts.map +1 -0
  107. package/dist/react/locale.js +158 -0
  108. package/dist/react/locale.js.map +1 -0
  109. package/dist/react/types.d.ts +130 -0
  110. package/dist/react/types.d.ts.map +1 -0
  111. package/dist/types/index.d.ts +24 -0
  112. package/dist/types/index.d.ts.map +1 -0
  113. package/dist/utils/getToolId.d.ts +9 -0
  114. package/dist/utils/getToolId.d.ts.map +1 -0
  115. package/dist/utils/getToolId.js +12 -0
  116. package/dist/utils/getToolId.js.map +1 -0
  117. package/dist/utils/mockData.d.ts +10 -0
  118. package/dist/utils/mockData.d.ts.map +1 -0
  119. package/dist/utils/mockData.js +61 -0
  120. package/dist/utils/mockData.js.map +1 -0
  121. package/dist/utils/snapCrosshair.d.ts +25 -0
  122. package/dist/utils/snapCrosshair.d.ts.map +1 -0
  123. package/dist/utils/validateCandle.d.ts +30 -0
  124. package/dist/utils/validateCandle.d.ts.map +1 -0
  125. package/dist/utils/validateCandle.js +21 -0
  126. package/dist/utils/validateCandle.js.map +1 -0
  127. package/examples/css/base.css +209 -0
  128. package/examples/css/chart.css +282 -0
  129. package/examples/css/indicators.css +255 -0
  130. package/examples/index.html +163 -0
  131. package/examples/js/chart.js +370 -0
  132. package/examples/js/indicators.js +27 -0
  133. package/examples/js/main.js +6 -0
  134. package/examples/js/ui.js +1641 -0
  135. package/lib/lightweight-charts.standalone.production.js +7 -0
  136. package/package.json +106 -0
  137. package/src/react/FullFeaturedChart.css +1007 -0
@@ -0,0 +1,1641 @@
1
+ /**
2
+ * 난독화된 lightweight-charts Line Tool 결과에서 도구 ID를 추출합니다.
3
+ */
4
+ function getToolIdFromResult(result) {
5
+ try {
6
+ return result?.ak?.ji ?? null;
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+
12
+ // Import from chart.js
13
+ import {
14
+ chart,
15
+ chartContainer,
16
+ candleSeries,
17
+ volumeSeries,
18
+ currentCandles,
19
+ indicatorSeries,
20
+ indicatorStates,
21
+ dynamicIndicatorSeries,
22
+ loadData,
23
+ toggleSMA20,
24
+ toggleEMA12,
25
+ toggleRSI14,
26
+ toggleMACD,
27
+ toggleBollingerBands
28
+ } from './chart.js';
29
+
30
+ // 시간봉 버튼 이벤트
31
+ document.querySelectorAll('.btn-timeframe').forEach(btn => {
32
+ btn.addEventListener('click', () => {
33
+ const timeFrame = btn.dataset.timeframe;
34
+
35
+ // active 클래스 토글
36
+ document.querySelectorAll('.btn-timeframe').forEach(b => b.classList.remove('active'));
37
+ btn.classList.add('active');
38
+
39
+ // 데이터 로드
40
+ loadData(timeFrame);
41
+
42
+ // 보조지표 다시 그리기
43
+ if (typeof applyAllIndicators === 'function') {
44
+ applyAllIndicators();
45
+ }
46
+ });
47
+ });
48
+
49
+ // 반응형
50
+ window.addEventListener('resize', () => {
51
+ chart.applyOptions({
52
+ width: chartContainer.clientWidth,
53
+ });
54
+ });
55
+
56
+ // ===== Snap Crosshair Plugin =====
57
+ let isShiftPressed = false;
58
+
59
+ // 커스텀 crosshair 요소 생성
60
+ const verticalLine = document.createElement('div');
61
+ verticalLine.style.cssText = 'position: absolute; width: 1px; height: 100%; top: 0; background: #2962FF; pointer-events: none; display: none; z-index: 1000;';
62
+ chartContainer.appendChild(verticalLine);
63
+
64
+ const horizontalLine = document.createElement('div');
65
+ horizontalLine.style.cssText = 'position: absolute; height: 1px; width: 100%; left: 0; background: #2962FF; pointer-events: none; display: none; z-index: 1000;';
66
+ chartContainer.appendChild(horizontalLine);
67
+
68
+ const priceLabel = document.createElement('div');
69
+ priceLabel.style.cssText = 'position: absolute; background: #2962FF; color: white; padding: 4px 8px; font-size: 11px; font-family: monospace; pointer-events: none; display: none; z-index: 1001; transform: translateY(-50%);';
70
+ chartContainer.appendChild(priceLabel);
71
+
72
+ // Shift 키 이벤트
73
+ window.addEventListener('keydown', (e) => {
74
+ if (e.key === 'Shift' && !isShiftPressed) {
75
+ isShiftPressed = true;
76
+ // 기본 crosshair 숨기기
77
+ chart.applyOptions({
78
+ crosshair: {
79
+ vertLine: { visible: false },
80
+ horzLine: { visible: false },
81
+ },
82
+ });
83
+ }
84
+ });
85
+
86
+ window.addEventListener('keyup', (e) => {
87
+ if (e.key === 'Shift') {
88
+ isShiftPressed = false;
89
+ // 기본 crosshair 복원
90
+ chart.applyOptions({
91
+ crosshair: {
92
+ vertLine: { visible: true },
93
+ horzLine: { visible: true },
94
+ },
95
+ });
96
+ // 커스텀 crosshair 숨기기
97
+ verticalLine.style.display = 'none';
98
+ horizontalLine.style.display = 'none';
99
+ priceLabel.style.display = 'none';
100
+ }
101
+ });
102
+
103
+ // Crosshair 이동 감지
104
+ chart.subscribeCrosshairMove((param) => {
105
+ if (!isShiftPressed || !param.point || !param.time) {
106
+ if (isShiftPressed) {
107
+ verticalLine.style.display = 'none';
108
+ horizontalLine.style.display = 'none';
109
+ priceLabel.style.display = 'none';
110
+ }
111
+ return;
112
+ }
113
+
114
+ // 현재 캔들 데이터
115
+ const candleData = param.seriesData.get(candleSeries);
116
+ if (!candleData) return;
117
+
118
+ const high = candleData.high;
119
+ const low = candleData.low;
120
+
121
+ // coordinateToPrice로 마우스 Y 좌표를 가격으로 변환 (더 정확함)
122
+ const mousePrice = candleSeries.coordinateToPrice(param.point.y);
123
+ if (mousePrice === null) return;
124
+
125
+ // 마우스 가격이 high와 low 중 어디에 더 가까운지 비교
126
+ const distToHigh = Math.abs(mousePrice - high);
127
+ const distToLow = Math.abs(mousePrice - low);
128
+
129
+ // 더 가까운 가격에 snap
130
+ const snapPrice = distToHigh < distToLow ? high : low;
131
+ const snapY = candleSeries.priceToCoordinate(snapPrice);
132
+
133
+ if (snapY === null) return;
134
+
135
+ // 커스텀 crosshair 표시
136
+ verticalLine.style.display = 'block';
137
+ verticalLine.style.left = param.point.x + 'px';
138
+
139
+ horizontalLine.style.display = 'block';
140
+ horizontalLine.style.top = snapY + 'px';
141
+
142
+ // 가격 라벨 - 오른쪽 가격 축에 배치
143
+ priceLabel.style.display = 'block';
144
+ priceLabel.style.top = snapY + 'px';
145
+ priceLabel.style.right = '0px';
146
+ priceLabel.textContent = snapPrice.toFixed(2);
147
+ });
148
+
149
+ // ===== Line Tools =====
150
+
151
+ // Line Tools 버튼 이벤트
152
+ function activateLineTool(toolType, btn) {
153
+ // 모든 Line Tool 버튼 비활성화
154
+ document.querySelectorAll('#trendLineToolBtn, #horizontalLineToolBtn, #verticalLineToolBtn, #rectangleToolBtn, #fibRetracementToolBtn, #textToolBtn').forEach(b => {
155
+ b.classList.remove('active');
156
+ });
157
+
158
+ // 이미 활성화된 도구를 다시 클릭하면 비활성화
159
+ if (activeLineTool === toolType) {
160
+ activeLineTool = null;
161
+ return;
162
+ }
163
+
164
+ // 새 도구 활성화
165
+ activeLineTool = toolType;
166
+ btn.classList.add('active');
167
+
168
+ // 마지막 설정값으로 Line Tool 추가
169
+ chart.addLineTool(toolType, [], {
170
+ line: {
171
+ width: lastToolOptions.lineWidth,
172
+ color: lastToolOptions.lineColor
173
+ }
174
+ });
175
+ }
176
+
177
+ document.getElementById('trendLineToolBtn').addEventListener('click', (e) => {
178
+ activateLineTool('TrendLine', e.target);
179
+ });
180
+
181
+ document.getElementById('horizontalLineToolBtn').addEventListener('click', (e) => {
182
+ activateLineTool('HorizontalLine', e.target);
183
+ });
184
+
185
+ document.getElementById('verticalLineToolBtn').addEventListener('click', (e) => {
186
+ activateLineTool('VerticalLine', e.target);
187
+ });
188
+
189
+ document.getElementById('rectangleToolBtn').addEventListener('click', (e) => {
190
+ activateLineTool('Rectangle', e.target);
191
+ });
192
+
193
+ document.getElementById('fibRetracementToolBtn').addEventListener('click', (e) => {
194
+ activateLineTool('FibRetracement', e.target);
195
+ });
196
+
197
+ document.getElementById('textToolBtn').addEventListener('click', (e) => {
198
+ // 모든 Line Tool 버튼 비활성화
199
+ document.querySelectorAll('#trendLineToolBtn, #horizontalLineToolBtn, #verticalLineToolBtn, #rectangleToolBtn, #fibRetracementToolBtn, #textToolBtn').forEach(b => {
200
+ b.classList.remove('active');
201
+ });
202
+
203
+ // 이미 활성화된 도구를 다시 클릭하면 비활성화
204
+ if (activeLineTool === 'Text') {
205
+ activeLineTool = null;
206
+ return;
207
+ }
208
+
209
+ // 새 도구 활성화
210
+ activeLineTool = 'Text';
211
+ e.target.classList.add('active');
212
+
213
+ // 텍스트 입력 받기 (커스텀 모달 사용)
214
+ showTextModal('Label').then((text) => {
215
+ if (text !== null && text !== '') {
216
+ // 마지막 설정값으로 Text 도구 추가
217
+ chart.addLineTool('Text', [], {
218
+ text: {
219
+ value: text,
220
+ font: {
221
+ color: lastToolOptions.lineColor,
222
+ size: lastToolOptions.lineWidth * 10 // 선 두께에 비례한 텍스트 크기
223
+ }
224
+ }
225
+ });
226
+ } else {
227
+ // 취소하면 버튼 비활성화
228
+ activeLineTool = null;
229
+ e.target.classList.remove('active');
230
+ }
231
+ });
232
+ });
233
+
234
+ // 모든 Line Tools 삭제
235
+ document.getElementById('removeAllLineToolsBtn').addEventListener('click', () => {
236
+ chart.removeAllLineTools();
237
+ lineToolsMap.clear(); // Map 초기화
238
+ selectedLineToolId = null;
239
+ activeLineTool = null;
240
+ // 모든 버튼 비활성화
241
+ document.querySelectorAll('#trendLineToolBtn, #horizontalLineToolBtn, #verticalLineToolBtn, #rectangleToolBtn, #fibRetracementToolBtn, #textToolBtn').forEach(b => {
242
+ b.classList.remove('active');
243
+ });
244
+ });
245
+
246
+
247
+ // ===== Context Menu for Line Tools =====
248
+ const contextMenu = document.getElementById('contextMenu');
249
+ const widthDisplay = document.getElementById('widthDisplay');
250
+ const colorCurrentBtn = document.getElementById('colorCurrentBtn');
251
+ const colorCurrentDisplay = document.getElementById('colorCurrentDisplay');
252
+ const colorPalette = document.getElementById('colorPalette');
253
+
254
+ // Line Tools 저장소 (직접 관리)
255
+ export let lineToolsMap = new Map(); // lineToolId -> { id, toolType, points, options }
256
+ export let selectedLineToolId = null;
257
+
258
+ // 마지막 설정값 저장 (새 도구에 적용)
259
+ export const lastToolOptions = {
260
+ lineWidth: 2,
261
+ lineColor: '#2962FF'
262
+ };
263
+
264
+ // 컨텍스트 메뉴 숨기기
265
+ function hideContextMenu() {
266
+ contextMenu.classList.remove('show');
267
+ }
268
+
269
+ // 컨텍스트 메뉴 표시
270
+ function showContextMenu(x, y, lineToolId) {
271
+ console.log('📋 showContextMenu 호출:', lineToolId);
272
+ const lineTool = lineToolsMap.get(lineToolId);
273
+ if (!lineTool) {
274
+ console.log('❌ lineToolsMap에서 도구를 찾을 수 없음:', lineToolId);
275
+ return;
276
+ }
277
+
278
+ console.log('✅ 도구 찾음:', lineTool.toolType);
279
+ selectedLineToolId = lineToolId;
280
+
281
+ // 텍스트 수정 버튼 표시/숨김
282
+ const editTextBtn = contextMenu.querySelector('[data-action="edit-text"]');
283
+ const editTextSeparator = editTextBtn.previousElementSibling;
284
+ if (lineTool.toolType === 'Text') {
285
+ editTextBtn.style.display = 'flex';
286
+ editTextSeparator.style.display = 'block';
287
+ } else {
288
+ editTextBtn.style.display = 'none';
289
+ editTextSeparator.style.display = 'none';
290
+ }
291
+
292
+ // 현재 색상 반영
293
+ let currentColor = '#2962FF';
294
+ if (lineTool.options?.line?.color) {
295
+ currentColor = lineTool.options.line.color;
296
+ } else if (lineTool.options?.text?.font?.color) {
297
+ currentColor = lineTool.options.text.font.color;
298
+ }
299
+
300
+ // 현재 색상 디스플레이 업데이트
301
+ colorCurrentDisplay.style.background = currentColor;
302
+
303
+ // 현재 선 두께 표시
304
+ const currentWidth = lineTool.options?.line?.width || 1;
305
+ widthDisplay.textContent = currentWidth;
306
+
307
+ // 메뉴 위치 설정
308
+ contextMenu.style.left = x + 'px';
309
+ contextMenu.style.top = y + 'px';
310
+ contextMenu.classList.add('show');
311
+ }
312
+
313
+ // 차트 외부 클릭 시 메뉴 숨기기
314
+ document.addEventListener('click', (e) => {
315
+ if (!contextMenu.contains(e.target)) {
316
+ hideContextMenu();
317
+ }
318
+ });
319
+
320
+ // ESC 키 이벤트
321
+ document.addEventListener('keydown', (e) => {
322
+ if (e.key === 'Escape') {
323
+ // 1. 컨텍스트 메뉴가 열려있을 때: 선택된 도구 삭제
324
+ if (contextMenu.classList.contains('show') && selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
325
+ console.log('🗑️ ESC - 선택된 도구 삭제:', selectedLineToolId);
326
+ // 기존 도구들을 모두 저장
327
+ const allTools = Array.from(lineToolsMap.values());
328
+
329
+ // 선택된 도구 제외
330
+ const remainingTools = allTools.filter(t => t.id !== selectedLineToolId);
331
+ console.log(' 남은 도구 개수:', remainingTools.length);
332
+
333
+ // 모든 Line Tools 제거
334
+ chart.removeAllLineTools();
335
+ lineToolsMap.clear();
336
+
337
+ // 남은 도구들 재생성
338
+ let firstNewId = null;
339
+ remainingTools.forEach((t, index) => {
340
+ const result = chart.addLineTool(t.toolType, t.points, t.options);
341
+
342
+ const newId = getToolIdFromResult(result);
343
+ if (newId) {
344
+ console.log(' 도구 재생성: 기존 ID', t.id, '→ 새 ID', newId);
345
+ lineToolsMap.set(newId, {
346
+ id: newId,
347
+ toolType: t.toolType,
348
+ points: t.points,
349
+ options: t.options
350
+ });
351
+
352
+ // 첫 번째 재생성된 도구의 ID 저장
353
+ if (index === 0) {
354
+ firstNewId = newId;
355
+ }
356
+ }
357
+ });
358
+
359
+ // 남은 도구가 있다면 첫 번째 도구를 자동으로 선택
360
+ if (firstNewId !== null) {
361
+ selectedLineToolId = firstNewId;
362
+ console.log(' ✅ 자동 선택된 도구 ID:', selectedLineToolId);
363
+ } else {
364
+ selectedLineToolId = null;
365
+ console.log(' ℹ️ 남은 도구가 없음');
366
+ }
367
+ hideContextMenu();
368
+ }
369
+ // 2. 그리기 도구가 활성화되어 있을 때: 그리기 취소 (점을 찍은 후에도 취소 가능)
370
+ else if (activeLineTool !== null) {
371
+ console.log('⏹️ ESC - 그리기 취소');
372
+ // 진행 중인 그리기 취소 - 모든 도구 제거 후 완성된 것만 재생성
373
+ const completedTools = Array.from(lineToolsMap.values());
374
+ console.log(' 완성된 도구 개수:', completedTools.length);
375
+ chart.removeAllLineTools();
376
+ lineToolsMap.clear();
377
+
378
+ // 완성된 도구들만 다시 추가
379
+ completedTools.forEach(t => {
380
+ const result = chart.addLineTool(t.toolType, t.points, t.options);
381
+ const newId = getToolIdFromResult(result);
382
+ if (newId) {
383
+ console.log(' 도구 재생성: 기존 ID', t.id, '→ 새 ID', newId);
384
+ // 기존 맵의 ID를 새 ID로 교체
385
+ lineToolsMap.set(newId, {
386
+ id: newId,
387
+ toolType: t.toolType,
388
+ points: t.points,
389
+ options: t.options
390
+ });
391
+ }
392
+ });
393
+ console.log(' 재생성 완료. lineToolsMap 크기:', lineToolsMap.size);
394
+
395
+ activeLineTool = null;
396
+ // 모든 Line Tool 버튼 비활성화
397
+ document.querySelectorAll('#trendLineToolBtn, #horizontalLineToolBtn, #verticalLineToolBtn, #rectangleToolBtn, #fibRetracementToolBtn, #textToolBtn').forEach(b => {
398
+ b.classList.remove('active');
399
+ });
400
+ }
401
+ }
402
+ });
403
+
404
+ // 메뉴 드래그 기능
405
+ let isDraggingMenu = false;
406
+ let menuDragOffsetX = 0;
407
+ let menuDragOffsetY = 0;
408
+
409
+ contextMenu.addEventListener('mousedown', (e) => {
410
+ // 버튼이나 색상 팔레트 클릭 시에는 드래그 안 함
411
+ if (e.target.classList.contains('context-menu-btn') ||
412
+ e.target.classList.contains('color-current-btn') ||
413
+ e.target.classList.contains('color-current-display') ||
414
+ e.target.classList.contains('color-option') ||
415
+ e.target.classList.contains('width-display')) {
416
+ return;
417
+ }
418
+
419
+ isDraggingMenu = true;
420
+ menuDragOffsetX = e.clientX - contextMenu.offsetLeft;
421
+ menuDragOffsetY = e.clientY - contextMenu.offsetTop;
422
+ contextMenu.classList.add('dragging');
423
+ e.preventDefault();
424
+ });
425
+
426
+ document.addEventListener('mousemove', (e) => {
427
+ if (!isDraggingMenu) return;
428
+
429
+ const x = e.clientX - menuDragOffsetX;
430
+ const y = e.clientY - menuDragOffsetY;
431
+
432
+ contextMenu.style.left = x + 'px';
433
+ contextMenu.style.top = y + 'px';
434
+ });
435
+
436
+ document.addEventListener('mouseup', () => {
437
+ if (isDraggingMenu) {
438
+ isDraggingMenu = false;
439
+ contextMenu.classList.remove('dragging');
440
+ }
441
+ });
442
+
443
+ // 차트에서 우클릭 이벤트 감지
444
+ chartContainer.addEventListener('contextmenu', (e) => {
445
+ e.preventDefault();
446
+ console.log('🖱️ 우클릭 이벤트 발생');
447
+ console.log(' 현재 selectedLineToolId:', selectedLineToolId);
448
+ console.log(' lineToolsMap.has(selectedLineToolId):', lineToolsMap.has(selectedLineToolId));
449
+ console.log(' lineToolsMap 전체 ID 목록:', Array.from(lineToolsMap.keys()));
450
+
451
+ if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
452
+ console.log('✅ 컨텍스트 메뉴 표시');
453
+ showContextMenu(e.clientX, e.clientY, selectedLineToolId);
454
+ } else if (selectedLineToolId === null && lineToolsMap.size > 0) {
455
+ // 선택된 도구가 없지만 lineToolsMap에 도구가 있는 경우
456
+ // 첫 번째 도구를 자동으로 선택
457
+ const firstToolId = Array.from(lineToolsMap.keys())[0];
458
+ console.log('⚠️ 도구가 선택되지 않음 → 첫 번째 도구 자동 선택:', firstToolId);
459
+ selectedLineToolId = firstToolId;
460
+ showContextMenu(e.clientX, e.clientY, firstToolId);
461
+ } else if (!lineToolsMap.has(selectedLineToolId) && lineToolsMap.size > 0) {
462
+ // selectedLineToolId가 있지만 lineToolsMap에 없는 경우 (ID가 변경됨)
463
+ const firstToolId = Array.from(lineToolsMap.keys())[0];
464
+ console.log('⚠️ 선택된 도구 ID가 유효하지 않음 → 첫 번째 도구 자동 선택:', firstToolId);
465
+ selectedLineToolId = firstToolId;
466
+ showContextMenu(e.clientX, e.clientY, firstToolId);
467
+ } else {
468
+ console.log('❌ 도구가 없음');
469
+ }
470
+ });
471
+
472
+ // Line Tool 이벤트 구독
473
+ let lastClickTime = 0;
474
+ let lastClickedToolId = null;
475
+ const DOUBLE_CLICK_THRESHOLD = 300; // 300ms
476
+
477
+ // subscribeLineToolsAfterEdit로 도구 저장 및 선택 추적
478
+ chart.subscribeLineToolsAfterEdit((params) => {
479
+ console.log('📍 LineToolsAfterEdit 이벤트:', params.stage, params);
480
+ const tool = params.selectedLineTool;
481
+ if (!tool) {
482
+ console.log('⚠️ tool이 없음');
483
+ return;
484
+ }
485
+
486
+ const toolId = tool.id;
487
+ console.log('🔧 도구 ID:', toolId, '타입:', tool.toolType, '스테이지:', params.stage);
488
+
489
+ // 모든 이벤트에서 도구 정보 저장/업데이트
490
+ lineToolsMap.set(toolId, {
491
+ id: toolId,
492
+ toolType: tool.toolType,
493
+ points: tool.points,
494
+ options: tool.options
495
+ });
496
+ console.log('💾 lineToolsMap 저장 완료. 현재 크기:', lineToolsMap.size);
497
+ console.log('📋 lineToolsMap 내용:', Array.from(lineToolsMap.keys()));
498
+
499
+ // 도구 생성 완료 시 버튼 비활성화
500
+ if (params.stage === 'lineToolFinished' || params.stage === 'pathFinished') {
501
+ activeLineTool = null;
502
+ document.querySelectorAll('#trendLineToolBtn, #horizontalLineToolBtn, #verticalLineToolBtn, #rectangleToolBtn, #fibRetracementToolBtn, #textToolBtn').forEach(b => {
503
+ b.classList.remove('active');
504
+ });
505
+
506
+ // 도구를 그리자마자 컨텍스트 메뉴 표시 (그린 위치 바로 아래)
507
+ setTimeout(() => {
508
+ if (lineToolsMap.has(toolId) && tool.points && tool.points.length > 0) {
509
+ // 마지막 포인트의 좌표를 픽셀로 변환
510
+ const lastPoint = tool.points[tool.points.length - 1];
511
+ const pixelY = candleSeries.priceToCoordinate(lastPoint.price);
512
+ const timeScale = chart.timeScale();
513
+ const pixelX = timeScale.timeToCoordinate(lastPoint.timestamp);
514
+
515
+ if (pixelX !== null && pixelY !== null) {
516
+ const rect = chartContainer.getBoundingClientRect();
517
+ // 도구 위치 바로 아래에 메뉴 표시
518
+ const menuX = rect.left + pixelX - 150; // 툴바 너비 절반 정도 왼쪽으로
519
+ const menuY = rect.top + pixelY + 10; // 도구 아래 10px
520
+ showContextMenu(menuX, menuY, toolId);
521
+ }
522
+ }
523
+ }, 100);
524
+ }
525
+
526
+ // 도구 클릭/선택 시
527
+ selectedLineToolId = toolId;
528
+ console.log('✅ selectedLineToolId 업데이트:', selectedLineToolId);
529
+
530
+ // 더블 클릭 감지 (텍스트 도구에만 적용)
531
+ const currentTime = Date.now();
532
+ if (currentTime - lastClickTime < DOUBLE_CLICK_THRESHOLD &&
533
+ lastClickedToolId === toolId &&
534
+ tool.toolType === 'Text') {
535
+ // 텍스트 수정 다이얼로그 표시 (커스텀 모달 사용)
536
+ const currentText = tool.options.text?.value || '';
537
+ showTextModal(currentText).then((newText) => {
538
+ if (newText !== null && newText !== '') {
539
+ const updatedOptions = {
540
+ ...tool.options,
541
+ text: {
542
+ ...tool.options.text,
543
+ value: newText,
544
+ }
545
+ };
546
+
547
+ // 기존 도구들을 모두 저장
548
+ const allTools = Array.from(lineToolsMap.values());
549
+
550
+ // 모든 Line Tools 제거
551
+ chart.removeAllLineTools();
552
+ lineToolsMap.clear();
553
+
554
+ // 업데이트된 도구 포함하여 모두 재생성
555
+ let lastNewId = null;
556
+ allTools.forEach(t => {
557
+ const opts = t.id === toolId ? updatedOptions : t.options;
558
+ const result = chart.addLineTool(t.toolType, t.points, opts);
559
+
560
+ const newId = getToolIdFromResult(result);
561
+ if (newId) {
562
+ lineToolsMap.set(newId, {
563
+ id: newId,
564
+ toolType: t.toolType,
565
+ points: t.points,
566
+ options: opts
567
+ });
568
+
569
+ // 업데이트된 도구의 새 ID 저장
570
+ if (t.id === toolId) {
571
+ lastNewId = newId;
572
+ }
573
+ }
574
+ });
575
+
576
+ // 재생성 후 업데이트된 도구의 새 ID를 선택
577
+ selectedLineToolId = lastNewId;
578
+ }
579
+ });
580
+ lastClickTime = 0; // 더블 클릭 후 리셋
581
+ lastClickedToolId = null;
582
+ } else {
583
+ lastClickTime = currentTime;
584
+ lastClickedToolId = toolId;
585
+ }
586
+ });
587
+
588
+ // 선 두께 변경 함수
589
+ function updateLineWidth(newWidth) {
590
+ console.log('📏 선 두께 변경:', newWidth);
591
+ if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
592
+ const tool = lineToolsMap.get(selectedLineToolId);
593
+ console.log(' 선택된 도구:', selectedLineToolId, tool.toolType);
594
+
595
+ let updatedOptions;
596
+ if (tool.toolType === 'Text') {
597
+ // 텍스트 도구의 경우 폰트 크기 업데이트
598
+ updatedOptions = {
599
+ ...tool.options,
600
+ text: {
601
+ ...tool.options.text,
602
+ font: {
603
+ ...tool.options.text?.font,
604
+ size: newWidth * 10, // 선 두께에 비례한 텍스트 크기
605
+ }
606
+ }
607
+ };
608
+ } else {
609
+ // 일반 도구의 경우 선 두께만 업데이트
610
+ updatedOptions = {
611
+ ...tool.options,
612
+ line: {
613
+ ...tool.options.line,
614
+ width: newWidth,
615
+ }
616
+ };
617
+ }
618
+
619
+ // 기존 도구들을 모두 저장
620
+ const allTools = Array.from(lineToolsMap.values());
621
+ console.log(' 재생성할 도구 개수:', allTools.length);
622
+
623
+ // 모든 Line Tools 제거
624
+ chart.removeAllLineTools();
625
+ lineToolsMap.clear();
626
+
627
+ // 업데이트된 도구 포함하여 모두 재생성
628
+ let lastNewId = null;
629
+ allTools.forEach(t => {
630
+ const opts = t.id === selectedLineToolId ? updatedOptions : t.options;
631
+ const result = chart.addLineTool(t.toolType, t.points, opts);
632
+
633
+ const newId = getToolIdFromResult(result);
634
+ if (newId) {
635
+ console.log(' 도구 재생성: 기존 ID', t.id, '→ 새 ID', newId, '타입:', t.toolType);
636
+ lineToolsMap.set(newId, {
637
+ id: newId,
638
+ toolType: t.toolType,
639
+ points: t.points,
640
+ options: opts
641
+ });
642
+
643
+ // 업데이트된 도구의 새 ID 저장
644
+ if (t.id === selectedLineToolId) {
645
+ lastNewId = newId;
646
+ console.log(' ✅ 선택된 도구의 새 ID:', newId);
647
+ }
648
+ }
649
+ });
650
+
651
+ // 재생성 후 업데이트된 도구의 새 ID를 선택
652
+ selectedLineToolId = lastNewId;
653
+ console.log(' 최종 selectedLineToolId:', selectedLineToolId);
654
+ console.log(' 최종 lineToolsMap 크기:', lineToolsMap.size);
655
+
656
+ // 마지막 설정값에 저장 (새 도구에 적용)
657
+ lastToolOptions.lineWidth = newWidth;
658
+
659
+ // 두께 표시 업데이트
660
+ widthDisplay.textContent = newWidth;
661
+ }
662
+ }
663
+
664
+ // 선 두께 감소 버튼
665
+ contextMenu.querySelector('[data-action="decrease-width"]').addEventListener('click', (e) => {
666
+ e.stopPropagation();
667
+ if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
668
+ const tool = lineToolsMap.get(selectedLineToolId);
669
+ const currentWidth = tool.options?.line?.width || 1;
670
+ const newWidth = Math.max(1, currentWidth - 1); // 최소 1
671
+ updateLineWidth(newWidth);
672
+ }
673
+ });
674
+
675
+ // 선 두께 증가 버튼
676
+ contextMenu.querySelector('[data-action="increase-width"]').addEventListener('click', (e) => {
677
+ e.stopPropagation();
678
+ if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
679
+ const tool = lineToolsMap.get(selectedLineToolId);
680
+ const currentWidth = tool.options?.line?.width || 1;
681
+ const newWidth = Math.min(10, currentWidth + 1); // 최대 10
682
+ updateLineWidth(newWidth);
683
+ }
684
+ });
685
+
686
+ // 색상 드롭다운 토글
687
+ colorCurrentBtn.addEventListener('click', (e) => {
688
+ e.stopPropagation();
689
+ colorPalette.classList.toggle('show');
690
+ });
691
+
692
+ // 색상 팔레트 외부 클릭시 닫기
693
+ document.addEventListener('click', (e) => {
694
+ if (!colorPalette.contains(e.target) && e.target !== colorCurrentBtn && e.target !== colorCurrentDisplay) {
695
+ colorPalette.classList.remove('show');
696
+ }
697
+ });
698
+
699
+ // 색상 옵션 버튼들
700
+ colorPalette.querySelectorAll('.color-option').forEach(btn => {
701
+ btn.addEventListener('click', (e) => {
702
+ e.stopPropagation();
703
+ const color = btn.dataset.color;
704
+
705
+ // 현재 색상 디스플레이 업데이트
706
+ colorCurrentDisplay.style.background = color;
707
+
708
+ // 팔레트 닫기
709
+ colorPalette.classList.remove('show');
710
+
711
+ if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
712
+ console.log('🎨 색상 변경:', color);
713
+ const tool = lineToolsMap.get(selectedLineToolId);
714
+ console.log(' 선택된 도구:', selectedLineToolId, tool.toolType);
715
+ let updatedOptions;
716
+
717
+ if (tool.toolType === 'Text') {
718
+ // 텍스트 도구는 font.color 변경
719
+ updatedOptions = {
720
+ ...tool.options,
721
+ text: {
722
+ ...tool.options.text,
723
+ font: {
724
+ ...tool.options.text.font,
725
+ color: color,
726
+ }
727
+ }
728
+ };
729
+ } else {
730
+ // 다른 도구들은 line.color 변경
731
+ updatedOptions = {
732
+ ...tool.options,
733
+ line: {
734
+ ...tool.options.line,
735
+ color: color,
736
+ }
737
+ };
738
+ }
739
+
740
+ // 기존 도구들을 모두 저장
741
+ const allTools = Array.from(lineToolsMap.values());
742
+ console.log(' 재생성할 도구 개수:', allTools.length);
743
+
744
+ // 모든 Line Tools 제거
745
+ chart.removeAllLineTools();
746
+ lineToolsMap.clear();
747
+
748
+ // 업데이트된 도구 포함하여 모두 재생성
749
+ let lastNewId = null;
750
+ allTools.forEach(t => {
751
+ const opts = t.id === selectedLineToolId ? updatedOptions : t.options;
752
+ const result = chart.addLineTool(t.toolType, t.points, opts);
753
+
754
+ const newId = getToolIdFromResult(result);
755
+ if (newId) {
756
+ console.log(' 도구 재생성: 기존 ID', t.id, '→ 새 ID', newId, '타입:', t.toolType);
757
+ lineToolsMap.set(newId, {
758
+ id: newId,
759
+ toolType: t.toolType,
760
+ points: t.points,
761
+ options: opts
762
+ });
763
+
764
+ // 업데이트된 도구의 새 ID 저장
765
+ if (t.id === selectedLineToolId) {
766
+ lastNewId = newId;
767
+ console.log(' ✅ 선택된 도구의 새 ID:', newId);
768
+ }
769
+ }
770
+ });
771
+
772
+ // 재생성 후 업데이트된 도구의 새 ID를 선택
773
+ selectedLineToolId = lastNewId;
774
+ console.log(' 최종 selectedLineToolId:', selectedLineToolId);
775
+ console.log(' 최종 lineToolsMap 크기:', lineToolsMap.size);
776
+
777
+ // 마지막 설정값에 저장 (새 도구에 적용)
778
+ lastToolOptions.lineColor = color;
779
+ }
780
+ });
781
+ });
782
+
783
+ // 텍스트 수정
784
+ contextMenu.querySelector('[data-action="edit-text"]').addEventListener('click', (e) => {
785
+ e.stopPropagation();
786
+ if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
787
+ const tool = lineToolsMap.get(selectedLineToolId);
788
+ if (tool.toolType === 'Text') {
789
+ const currentText = tool.options.text?.value || '';
790
+ showTextModal(currentText).then((newText) => {
791
+ if (newText !== null && newText !== '') {
792
+ const updatedOptions = {
793
+ ...tool.options,
794
+ text: {
795
+ ...tool.options.text,
796
+ value: newText,
797
+ }
798
+ };
799
+
800
+ // 기존 도구들을 모두 저장
801
+ const allTools = Array.from(lineToolsMap.values());
802
+
803
+ // 모든 Line Tools 제거
804
+ chart.removeAllLineTools();
805
+ lineToolsMap.clear();
806
+
807
+ // 업데이트된 도구 포함하여 모두 재생성
808
+ let lastNewId = null;
809
+ allTools.forEach(t => {
810
+ const opts = t.id === selectedLineToolId ? updatedOptions : t.options;
811
+ const result = chart.addLineTool(t.toolType, t.points, opts);
812
+
813
+ const newId = getToolIdFromResult(result);
814
+ if (newId) {
815
+ lineToolsMap.set(newId, {
816
+ id: newId,
817
+ toolType: t.toolType,
818
+ points: t.points,
819
+ options: opts
820
+ });
821
+
822
+ // 업데이트된 도구의 새 ID 저장
823
+ if (t.id === selectedLineToolId) {
824
+ lastNewId = newId;
825
+ }
826
+ }
827
+ });
828
+
829
+ // 재생성 후 업데이트된 도구의 새 ID를 선택
830
+ selectedLineToolId = lastNewId;
831
+ }
832
+ });
833
+ }
834
+ }
835
+ hideContextMenu();
836
+ });
837
+
838
+ // 삭제
839
+ contextMenu.querySelector('[data-action="delete"]').addEventListener('click', (e) => {
840
+ e.stopPropagation();
841
+ console.log('🗑️ 삭제 버튼 클릭');
842
+ if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
843
+ console.log(' 삭제할 도구 ID:', selectedLineToolId);
844
+ // 기존 도구들을 모두 저장
845
+ const allTools = Array.from(lineToolsMap.values());
846
+ console.log(' 전체 도구 개수:', allTools.length);
847
+
848
+ // 선택된 도구 제외
849
+ const remainingTools = allTools.filter(t => t.id !== selectedLineToolId);
850
+ console.log(' 남은 도구 개수:', remainingTools.length);
851
+
852
+ // 모든 Line Tools 제거
853
+ chart.removeAllLineTools();
854
+ lineToolsMap.clear();
855
+
856
+ // 남은 도구들 재생성
857
+ let firstNewId = null;
858
+ remainingTools.forEach((t, index) => {
859
+ const result = chart.addLineTool(t.toolType, t.points, t.options);
860
+
861
+ const newId = getToolIdFromResult(result);
862
+ if (newId) {
863
+ console.log(' 도구 재생성: 기존 ID', t.id, '→ 새 ID', newId, '타입:', t.toolType);
864
+ lineToolsMap.set(newId, {
865
+ id: newId,
866
+ toolType: t.toolType,
867
+ points: t.points,
868
+ options: t.options
869
+ });
870
+
871
+ // 첫 번째 재생성된 도구의 ID 저장
872
+ if (index === 0) {
873
+ firstNewId = newId;
874
+ }
875
+ }
876
+ });
877
+
878
+ console.log(' 삭제 완료. 최종 lineToolsMap 크기:', lineToolsMap.size);
879
+
880
+ // 남은 도구가 있다면 첫 번째 도구를 자동으로 선택
881
+ if (firstNewId !== null) {
882
+ selectedLineToolId = firstNewId;
883
+ console.log(' ✅ 자동 선택된 도구 ID:', selectedLineToolId);
884
+ } else {
885
+ selectedLineToolId = null;
886
+ console.log(' ℹ️ 남은 도구가 없음');
887
+ }
888
+ }
889
+ hideContextMenu();
890
+ });
891
+
892
+ // ===== Dropdown Menu Functionality =====
893
+ const indicatorBtn = document.getElementById('indicatorBtn');
894
+ const indicatorMenu = document.getElementById('indicatorMenu');
895
+ const drawingBtn = document.getElementById('drawingBtn');
896
+ const drawingMenu = document.getElementById('drawingMenu');
897
+
898
+ // Toggle indicator dropdown
899
+ indicatorBtn.addEventListener('click', (e) => {
900
+ e.stopPropagation();
901
+ indicatorMenu.classList.toggle('show');
902
+ drawingMenu.classList.remove('show');
903
+ });
904
+
905
+ // Toggle drawing dropdown
906
+ drawingBtn.addEventListener('click', (e) => {
907
+ e.stopPropagation();
908
+ drawingMenu.classList.toggle('show');
909
+ indicatorMenu.classList.remove('show');
910
+ });
911
+
912
+ // Close dropdowns and color palettes when clicking outside
913
+ document.addEventListener('click', () => {
914
+ indicatorMenu.classList.remove('show');
915
+ drawingMenu.classList.remove('show');
916
+ // 색상 팔레트 닫기
917
+ document.querySelectorAll('.color-palette-popup').forEach(p => p.remove());
918
+ document.querySelectorAll('.period-color-picker').forEach(b => b.classList.remove('palette-active'));
919
+ });
920
+
921
+ // Prevent dropdown from closing when clicking inside menu (except for menu items)
922
+ indicatorMenu.addEventListener('click', (e) => {
923
+ if (!e.target.classList.contains('dropdown-item')) {
924
+ e.stopPropagation();
925
+ }
926
+ });
927
+ drawingMenu.addEventListener('click', (e) => {
928
+ if (!e.target.classList.contains('dropdown-item')) {
929
+ e.stopPropagation();
930
+ }
931
+ });
932
+
933
+ // Connect dropdown items to existing button handlers
934
+ // Drawing tools
935
+ document.querySelectorAll('#drawingMenu .dropdown-item').forEach(item => {
936
+ item.addEventListener('click', () => {
937
+ const btnId = item.id;
938
+ const originalBtn = document.getElementById(btnId);
939
+ if (originalBtn) {
940
+ originalBtn.click();
941
+ }
942
+ // Close dropdown after selection
943
+ drawingMenu.classList.remove('show');
944
+ });
945
+ });
946
+
947
+ // ===== Indicator Settings in Dropdown =====
948
+ const indicatorSettingsSide = document.getElementById('indicatorSettingsSide');
949
+
950
+ // 지표별 설정 저장
951
+ export const indicatorConfigs = {
952
+ sma: [],
953
+ ema: [],
954
+ rsi: [],
955
+ macd: [],
956
+ bbands: []
957
+ };
958
+
959
+ // 지표별 기본 정보
960
+ export const indicatorInfo = {
961
+ sma: { title: '이동평균선', desc: '지난 n일 동안 주가 평균값을 이은 선', defaultValue: 20 },
962
+ ema: { title: '지수이동평균', desc: '최근 가격에 더 큰 가중치를 둔 이동평균선', defaultValue: 12 },
963
+ rsi: { title: 'RSI', desc: '상대강도지수 - 과매수/과매도 판단', defaultValue: 14 },
964
+ macd: { title: 'MACD', desc: '이동평균 수렴확산 - 추세 전환 신호', defaultValue: 26 },
965
+ bbands: { title: '볼린저 밴드', desc: '가격 변동성을 나타내는 밴드', defaultValue: 20 }
966
+ };
967
+
968
+ // 색상 팔레트 (32색)
969
+ export const colorPaletteOptions = [
970
+ // Row 1: 어두운 톤
971
+ '#3C4043', '#1A73E8', '#9334E6', '#B80000', '#E37400', '#F9AB00', '#007B83', '#1E8E3E',
972
+ // Row 2: 기본 톤
973
+ '#5F6368', '#4285F4', '#A142F4', '#D93025', '#F57C00', '#F9AB00', '#12B5CB', '#34A853',
974
+ // Row 3: 밝은 톤
975
+ '#9AA0A6', '#8AB4F8', '#C58AF9', '#EE675C', '#FF8A65', '#FFB300', '#4DB6AC', '#81C995',
976
+ // Row 4: 연한 톤
977
+ '#DADCE0', '#AECBFA', '#D7AEFB', '#F28B82', '#FFAB91', '#FFD54F', '#80DEEA', '#A5D6A7'
978
+ ];
979
+
980
+ // 기본 색상 (기존 호환용)
981
+ export const colorOptions = ['#26a69a', '#ef5350', '#2196f3', '#ff6f00', '#ab47bc', '#66bb6a', '#ffa726', '#42a5f5'];
982
+
983
+ export let currentSelectedIndicator = null;
984
+
985
+ // 지표 아이템 클릭 이벤트
986
+ document.querySelectorAll('.indicator-item').forEach(item => {
987
+ item.addEventListener('click', (e) => {
988
+ // 체크박스 클릭이면 토글
989
+ if (e.target.classList.contains('indicator-checkbox')) {
990
+ item.classList.toggle('checked');
991
+ const indicator = item.dataset.indicator;
992
+
993
+ if (!item.classList.contains('checked')) {
994
+ // 체크 해제 - 지표 삭제
995
+ indicatorConfigs[indicator] = [];
996
+ applyAllIndicators(); // 즉시 차트에서 제거
997
+ } else {
998
+ // 체크 - 기본 설정 추가
999
+ if (indicatorConfigs[indicator].length === 0) {
1000
+ // 이동평균선과 지수이동평균은 4개 기간 기본값
1001
+ if (indicator === 'sma' || indicator === 'ema') {
1002
+ const periods = [5, 20, 60, 120];
1003
+ periods.forEach((period, idx) => {
1004
+ indicatorConfigs[indicator].push({
1005
+ color: colorOptions[idx % colorOptions.length],
1006
+ thickness: 1,
1007
+ source: 'close',
1008
+ value: period
1009
+ });
1010
+ });
1011
+ } else if (indicator === 'bbands') {
1012
+ // 볼린저 밴드는 각 라인 색상 설정 포함
1013
+ indicatorConfigs[indicator].push({
1014
+ color: colorOptions[0],
1015
+ thickness: 2,
1016
+ source: 'close',
1017
+ value: indicatorInfo[indicator].defaultValue,
1018
+ upperColor: '#F23645',
1019
+ middleColor: '#2962FF',
1020
+ lowerColor: '#089981',
1021
+ stdDev: 2
1022
+ });
1023
+ } else {
1024
+ // 다른 지표는 기본값 하나만
1025
+ const thickness = (indicator === 'rsi' || indicator === 'macd') ? 2 : 1;
1026
+ indicatorConfigs[indicator].push({
1027
+ color: colorOptions[0],
1028
+ thickness: thickness,
1029
+ source: 'close',
1030
+ value: indicatorInfo[indicator].defaultValue
1031
+ });
1032
+ }
1033
+ applyAllIndicators(); // 즉시 차트에 추가
1034
+ }
1035
+ }
1036
+ }
1037
+
1038
+ // 선택 상태 업데이트
1039
+ document.querySelectorAll('.indicator-item').forEach(i => i.classList.remove('selected'));
1040
+ item.classList.add('selected');
1041
+ currentSelectedIndicator = item.dataset.indicator;
1042
+
1043
+ // 오른쪽 설정 화면 렌더링
1044
+ renderIndicatorSettings(currentSelectedIndicator);
1045
+ });
1046
+ });
1047
+
1048
+ function renderIndicatorSettings(indicator) {
1049
+ if (!indicator) {
1050
+ indicatorSettingsSide.innerHTML = '<div class="indicator-empty-state">지표를 선택하세요</div>';
1051
+ return;
1052
+ }
1053
+
1054
+ const info = indicatorInfo[indicator];
1055
+ const configs = indicatorConfigs[indicator];
1056
+
1057
+ let html = `
1058
+ <div class="indicator-settings-title">${info.title}</div>
1059
+ <div class="indicator-settings-desc">${info.desc}</div>
1060
+ `;
1061
+
1062
+ if (configs.length === 0) {
1063
+ html += '<div class="indicator-empty-state">체크박스를 클릭하여 지표를 활성화하세요</div>';
1064
+ } else {
1065
+ // 하단 지표 (RSI, MACD)는 간단한 UI
1066
+ if (indicator === 'rsi') {
1067
+ const config = configs[0];
1068
+ html += `
1069
+ <div style="margin-top: 16px;">
1070
+ <div style="display: flex; gap: 12px; align-items: flex-start;">
1071
+ <div style="flex: 1;">
1072
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">색상</div>
1073
+ <div class="period-color-picker rsi-color-picker" data-indicator="${indicator}" data-index="0" style="background: ${config.color}; width: 100%; height: 40px;"></div>
1074
+ </div>
1075
+ <div style="flex: 1;">
1076
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">기간</div>
1077
+ <input type="number" class="period-value-field" data-indicator="${indicator}" data-index="0" value="${config.value}" min="1" max="500" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
1078
+ </div>
1079
+ </div>
1080
+ </div>
1081
+ `;
1082
+ } else if (indicator === 'macd') {
1083
+ const config = configs[0];
1084
+ html += `
1085
+ <div style="margin-top: 16px;">
1086
+ <!-- 색상 설정 (한 줄) -->
1087
+ <div style="display: flex; gap: 8px; margin-bottom: 12px;">
1088
+ <div style="flex: 1;">
1089
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">MACD</div>
1090
+ <div class="period-color-picker macd-line-color-picker" data-macd-type="line" style="background: #2962FF; width: 100%; height: 40px;"></div>
1091
+ </div>
1092
+ <div style="flex: 1;">
1093
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">Signal</div>
1094
+ <div class="period-color-picker macd-signal-color-picker" data-macd-type="signal" style="background: #FF6D00; width: 100%; height: 40px;"></div>
1095
+ </div>
1096
+ </div>
1097
+
1098
+ <!-- 파라미터 설정 (한 줄) -->
1099
+ <div style="display: flex; gap: 8px;">
1100
+ <div style="flex: 1;">
1101
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">단기</div>
1102
+ <input type="number" class="macd-fast-field" value="12" min="1" max="500" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
1103
+ </div>
1104
+ <div style="flex: 1;">
1105
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">장기</div>
1106
+ <input type="number" class="macd-slow-field" value="26" min="1" max="500" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
1107
+ </div>
1108
+ <div style="flex: 1;">
1109
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">시그널</div>
1110
+ <input type="number" class="macd-signal-field" value="9" min="1" max="500" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
1111
+ </div>
1112
+ </div>
1113
+ </div>
1114
+ `;
1115
+ } else if (indicator === 'bbands') {
1116
+ const config = configs[0];
1117
+ html += `
1118
+ <div style="margin-top: 16px;">
1119
+ <!-- 색상 설정 (한 줄) -->
1120
+ <div style="display: flex; gap: 8px; margin-bottom: 12px;">
1121
+ <div style="flex: 1;">
1122
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">상단</div>
1123
+ <div class="period-color-picker bbands-upper-color-picker" data-bbands-type="upper" style="background: ${config.upperColor || '#F23645'}; width: 100%; height: 40px;"></div>
1124
+ </div>
1125
+ <div style="flex: 1;">
1126
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">중간</div>
1127
+ <div class="period-color-picker bbands-middle-color-picker" data-bbands-type="middle" style="background: ${config.middleColor || '#2962FF'}; width: 100%; height: 40px;"></div>
1128
+ </div>
1129
+ <div style="flex: 1;">
1130
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">하단</div>
1131
+ <div class="period-color-picker bbands-lower-color-picker" data-bbands-type="lower" style="background: ${config.lowerColor || '#089981'}; width: 100%; height: 40px;"></div>
1132
+ </div>
1133
+ </div>
1134
+
1135
+ <!-- 파라미터 설정 (한 줄) -->
1136
+ <div style="display: flex; gap: 8px;">
1137
+ <div style="flex: 1;">
1138
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">기간</div>
1139
+ <input type="number" class="bbands-period-field" value="${config.value || 20}" min="1" max="500" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
1140
+ </div>
1141
+ <div style="flex: 1;">
1142
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">표준편차</div>
1143
+ <input type="number" class="bbands-stddev-field" value="${config.stdDev || 2}" min="0.5" max="5" step="0.1" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
1144
+ </div>
1145
+ <div style="flex: 1;">
1146
+ <div style="font-size: 13px; color: #666; margin-bottom: 8px;">두께</div>
1147
+ <div class="bbands-thickness-display" style="width: 100%; height: 40px; border: 1px solid #e0e0e0; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 500; cursor: pointer; user-select: none; background: white;">${config.thickness || 2}px</div>
1148
+ </div>
1149
+ </div>
1150
+ </div>
1151
+ `;
1152
+ } else {
1153
+ // 상단 지표 (SMA, EMA, BBands)는 기존 UI
1154
+ configs.forEach((config, index) => {
1155
+ html += `
1156
+ <div class="indicator-period-row">
1157
+ <span class="period-label-col">기간${index + 1}</span>
1158
+ <div class="period-color-picker" data-indicator="${indicator}" data-index="${index}" style="background: ${config.color};"></div>
1159
+ <div class="period-thickness-display" data-indicator="${indicator}" data-index="${index}">${config.thickness}px</div>
1160
+ <select class="period-source-dropdown" data-indicator="${indicator}" data-index="${index}">
1161
+ <option value="close" ${config.source === 'close' ? 'selected' : ''}>종가</option>
1162
+ <option value="open" ${config.source === 'open' ? 'selected' : ''}>시가</option>
1163
+ <option value="high" ${config.source === 'high' ? 'selected' : ''}>고가</option>
1164
+ <option value="low" ${config.source === 'low' ? 'selected' : ''}>저가</option>
1165
+ </select>
1166
+ <input type="number" class="period-value-field" data-indicator="${indicator}" data-index="${index}" value="${config.value}" min="1" max="500">
1167
+ ${configs.length > 1 ? `<button class="period-delete-btn" data-indicator="${indicator}" data-index="${index}">×</button>` : ''}
1168
+ </div>
1169
+ `;
1170
+ });
1171
+
1172
+ html += `
1173
+ <button class="add-period-button" data-indicator="${indicator}">
1174
+ + 기간 추가
1175
+ </button>
1176
+ `;
1177
+ }
1178
+ }
1179
+
1180
+ indicatorSettingsSide.innerHTML = html;
1181
+ attachIndicatorSettingsEvents();
1182
+ }
1183
+
1184
+ function attachIndicatorSettingsEvents() {
1185
+ // 색상 변경 - 팔레트 표시
1186
+ document.querySelectorAll('.period-color-picker').forEach(btn => {
1187
+ btn.addEventListener('click', (e) => {
1188
+ e.stopPropagation();
1189
+ const indicator = btn.dataset.indicator;
1190
+ const index = parseInt(btn.dataset.index);
1191
+
1192
+ // 현재 색상 가져오기
1193
+ let currentColor;
1194
+ if (btn.classList.contains('macd-line-color-picker') || btn.classList.contains('macd-signal-color-picker')) {
1195
+ currentColor = btn.style.background;
1196
+ } else if (indicator && index !== undefined) {
1197
+ currentColor = indicatorConfigs[indicator][index].color;
1198
+ } else {
1199
+ currentColor = '#26a69a';
1200
+ }
1201
+
1202
+ // 이미 이 버튼이 팔레트를 열어놓았는지 확인
1203
+ const existingPalette = document.querySelector('.color-palette-popup');
1204
+ if (existingPalette && btn.classList.contains('palette-active')) {
1205
+ // 같은 버튼 다시 클릭 시 팔레트 닫기
1206
+ existingPalette.remove();
1207
+ btn.classList.remove('palette-active');
1208
+ return;
1209
+ }
1210
+
1211
+ // 기존 팔레트 제거 및 모든 버튼의 active 상태 제거
1212
+ document.querySelectorAll('.color-palette-popup').forEach(p => p.remove());
1213
+ document.querySelectorAll('.period-color-picker').forEach(b => b.classList.remove('palette-active'));
1214
+
1215
+ // 현재 버튼을 active로 표시
1216
+ btn.classList.add('palette-active');
1217
+
1218
+ // 버튼 위치 계산
1219
+ const rect = btn.getBoundingClientRect();
1220
+
1221
+ // 팔레트 HTML 생성 (버튼 바로 아래)
1222
+ let paletteHTML = `
1223
+ <div class="color-palette-popup show" style="top: ${rect.bottom + 5}px; left: ${rect.left}px;">
1224
+ <div class="color-palette-title">컬러</div>
1225
+ <div class="color-palette-grid">
1226
+ `;
1227
+
1228
+ colorPaletteOptions.forEach(color => {
1229
+ const selected = color === currentColor ? 'selected' : '';
1230
+ paletteHTML += `<div class="color-palette-item ${selected}" style="background: ${color};" data-color="${color}"></div>`;
1231
+ });
1232
+
1233
+ paletteHTML += `
1234
+ </div>
1235
+ </div>
1236
+ `;
1237
+
1238
+ // body에 팔레트 추가
1239
+ document.body.insertAdjacentHTML('beforeend', paletteHTML);
1240
+
1241
+ // 팔레트 아이템 클릭 이벤트
1242
+ const palette = document.querySelector('.color-palette-popup');
1243
+ palette.querySelectorAll('.color-palette-item').forEach(item => {
1244
+ item.addEventListener('click', (e) => {
1245
+ e.stopPropagation();
1246
+ const selectedColor = item.dataset.color;
1247
+
1248
+ // MACD 색상 picker인 경우
1249
+ if (btn.classList.contains('macd-line-color-picker') || btn.classList.contains('macd-signal-color-picker')) {
1250
+ btn.style.background = selectedColor;
1251
+ palette.remove();
1252
+ btn.classList.remove('palette-active');
1253
+ applyAllIndicators();
1254
+ } else if (btn.classList.contains('bbands-upper-color-picker') ||
1255
+ btn.classList.contains('bbands-middle-color-picker') ||
1256
+ btn.classList.contains('bbands-lower-color-picker')) {
1257
+ // 볼린저 밴드 색상 picker인 경우
1258
+ btn.style.background = selectedColor;
1259
+ const bbandsType = btn.dataset.bbandsType;
1260
+ const config = indicatorConfigs['bbands'][0];
1261
+ if (bbandsType === 'upper') {
1262
+ config.upperColor = selectedColor;
1263
+ } else if (bbandsType === 'middle') {
1264
+ config.middleColor = selectedColor;
1265
+ } else if (bbandsType === 'lower') {
1266
+ config.lowerColor = selectedColor;
1267
+ }
1268
+ palette.remove();
1269
+ btn.classList.remove('palette-active');
1270
+ applyAllIndicators();
1271
+ } else if (indicator && index !== undefined) {
1272
+ // 일반 지표인 경우
1273
+ indicatorConfigs[indicator][index].color = selectedColor;
1274
+ palette.remove();
1275
+ btn.classList.remove('palette-active');
1276
+ renderIndicatorSettings(indicator);
1277
+ applyAllIndicators();
1278
+ }
1279
+ });
1280
+ });
1281
+ });
1282
+ });
1283
+
1284
+ // 두께 변경
1285
+ document.querySelectorAll('.period-thickness-display').forEach(btn => {
1286
+ btn.addEventListener('click', () => {
1287
+ const indicator = btn.dataset.indicator;
1288
+ const index = parseInt(btn.dataset.index);
1289
+ indicatorConfigs[indicator][index].thickness = (indicatorConfigs[indicator][index].thickness % 5) + 1;
1290
+ renderIndicatorSettings(indicator);
1291
+ applyAllIndicators(); // 즉시 차트에 반영
1292
+ });
1293
+ });
1294
+
1295
+ // 소스 변경
1296
+ document.querySelectorAll('.period-source-dropdown').forEach(select => {
1297
+ select.addEventListener('change', (e) => {
1298
+ const indicator = select.dataset.indicator;
1299
+ const index = parseInt(select.dataset.index);
1300
+ indicatorConfigs[indicator][index].source = e.target.value;
1301
+ applyAllIndicators(); // 즉시 차트에 반영
1302
+ });
1303
+ });
1304
+
1305
+ // 값 변경
1306
+ document.querySelectorAll('.period-value-field').forEach(input => {
1307
+ input.addEventListener('change', (e) => {
1308
+ const indicator = input.dataset.indicator;
1309
+ const index = parseInt(input.dataset.index);
1310
+ indicatorConfigs[indicator][index].value = parseInt(e.target.value) || 1;
1311
+ applyAllIndicators(); // 즉시 차트에 반영
1312
+ });
1313
+ });
1314
+
1315
+ // 삭제
1316
+ document.querySelectorAll('.period-delete-btn').forEach(btn => {
1317
+ btn.addEventListener('click', () => {
1318
+ const indicator = btn.dataset.indicator;
1319
+ const index = parseInt(btn.dataset.index);
1320
+ indicatorConfigs[indicator].splice(index, 1);
1321
+ renderIndicatorSettings(indicator);
1322
+ applyAllIndicators(); // 즉시 차트에 반영
1323
+ });
1324
+ });
1325
+
1326
+ // 기간 추가
1327
+ document.querySelectorAll('.add-period-button').forEach(btn => {
1328
+ btn.addEventListener('click', () => {
1329
+ const indicator = btn.dataset.indicator;
1330
+ const lastConfig = indicatorConfigs[indicator][indicatorConfigs[indicator].length - 1];
1331
+ const nextColorIdx = (colorOptions.indexOf(lastConfig.color) + 1) % colorOptions.length;
1332
+ indicatorConfigs[indicator].push({
1333
+ color: colorOptions[nextColorIdx],
1334
+ thickness: 1,
1335
+ source: 'close',
1336
+ value: indicatorInfo[indicator].defaultValue
1337
+ });
1338
+ renderIndicatorSettings(indicator);
1339
+ applyAllIndicators(); // 즉시 차트에 반영
1340
+ });
1341
+ });
1342
+
1343
+ // MACD 파라미터 변경
1344
+ const macdFast = document.querySelector('.macd-fast-field');
1345
+ const macdSlow = document.querySelector('.macd-slow-field');
1346
+ const macdSignal = document.querySelector('.macd-signal-field');
1347
+
1348
+ if (macdFast) {
1349
+ macdFast.addEventListener('change', () => {
1350
+ applyAllIndicators();
1351
+ });
1352
+ }
1353
+ if (macdSlow) {
1354
+ macdSlow.addEventListener('change', () => {
1355
+ applyAllIndicators();
1356
+ });
1357
+ }
1358
+ if (macdSignal) {
1359
+ macdSignal.addEventListener('change', () => {
1360
+ applyAllIndicators();
1361
+ });
1362
+ }
1363
+
1364
+ // Bollinger Bands 파라미터 변경
1365
+ const bbandsPeriod = document.querySelector('.bbands-period-field');
1366
+ const bbandsStdDev = document.querySelector('.bbands-stddev-field');
1367
+ const bbandsThickness = document.querySelector('.bbands-thickness-display');
1368
+
1369
+ if (bbandsPeriod) {
1370
+ bbandsPeriod.addEventListener('change', (e) => {
1371
+ const config = indicatorConfigs['bbands'][0];
1372
+ config.value = parseInt(e.target.value) || 20;
1373
+ applyAllIndicators();
1374
+ });
1375
+ }
1376
+ if (bbandsStdDev) {
1377
+ bbandsStdDev.addEventListener('change', (e) => {
1378
+ const config = indicatorConfigs['bbands'][0];
1379
+ config.stdDev = parseFloat(e.target.value) || 2;
1380
+ applyAllIndicators();
1381
+ });
1382
+ }
1383
+ if (bbandsThickness) {
1384
+ bbandsThickness.addEventListener('click', () => {
1385
+ const config = indicatorConfigs['bbands'][0];
1386
+ config.thickness = (config.thickness % 5) + 1;
1387
+ renderIndicatorSettings('bbands');
1388
+ applyAllIndicators();
1389
+ });
1390
+ }
1391
+ }
1392
+
1393
+ // 드롭다운이 닫힐 때 체크박스 상태 동기화
1394
+ document.addEventListener('click', (e) => {
1395
+ if (!indicatorMenu.contains(e.target) && !indicatorBtn.contains(e.target)) {
1396
+ if (indicatorMenu.classList.contains('show')) {
1397
+ // 체크박스 상태 기반으로 indicatorConfigs 재구성
1398
+ document.querySelectorAll('.indicator-item').forEach(item => {
1399
+ const indicator = item.dataset.indicator;
1400
+ if (item.classList.contains('checked')) {
1401
+ // 체크되어 있는데 설정이 비어있으면 기본값 추가
1402
+ if (indicatorConfigs[indicator].length === 0) {
1403
+ // 이동평균선과 지수이동평균은 4개 기간 기본값
1404
+ if (indicator === 'sma' || indicator === 'ema') {
1405
+ const periods = [5, 20, 60, 120];
1406
+ periods.forEach((period, idx) => {
1407
+ indicatorConfigs[indicator].push({
1408
+ color: colorOptions[idx % colorOptions.length],
1409
+ thickness: 1,
1410
+ source: 'close',
1411
+ value: period
1412
+ });
1413
+ });
1414
+ } else if (indicator === 'bbands') {
1415
+ // 볼린저 밴드는 각 라인 색상 설정 포함
1416
+ indicatorConfigs[indicator].push({
1417
+ color: colorOptions[0],
1418
+ thickness: 2,
1419
+ source: 'close',
1420
+ value: indicatorInfo[indicator].defaultValue,
1421
+ upperColor: '#F23645',
1422
+ middleColor: '#2962FF',
1423
+ lowerColor: '#089981',
1424
+ stdDev: 2
1425
+ });
1426
+ } else {
1427
+ // 다른 지표는 기본값 하나만
1428
+ const thickness = (indicator === 'rsi' || indicator === 'macd') ? 2 : 1;
1429
+ indicatorConfigs[indicator].push({
1430
+ color: colorOptions[0],
1431
+ thickness: thickness,
1432
+ source: 'close',
1433
+ value: indicatorInfo[indicator].defaultValue
1434
+ });
1435
+ }
1436
+ }
1437
+ } else {
1438
+ // 체크 해제되어 있으면 설정 비우기
1439
+ indicatorConfigs[indicator] = [];
1440
+ }
1441
+ });
1442
+
1443
+ indicatorMenu.classList.remove('show');
1444
+ }
1445
+ }
1446
+ });
1447
+
1448
+ function applyAllIndicators() {
1449
+ // 기존 동적 지표 제거
1450
+ window.dynamicIndicatorSeries.forEach(series => {
1451
+ window.chart.removeSeries(series);
1452
+ });
1453
+ window.dynamicIndicatorSeries.length = 0; // 배열 비우기
1454
+
1455
+ Object.keys(indicatorConfigs).forEach(type => {
1456
+ indicatorConfigs[type].forEach(config => {
1457
+ let data;
1458
+
1459
+ if (type === 'sma') {
1460
+ data = calculateSMA(window.currentCandles, { period: config.value });
1461
+ if (data && data.length > 0) {
1462
+ const series = window.chart.addLineSeries({
1463
+ color: config.color,
1464
+ lineWidth: config.thickness,
1465
+ title: `SMA ${config.value}`,
1466
+ });
1467
+ series.setData(data);
1468
+ window.dynamicIndicatorSeries.push(series);
1469
+ }
1470
+ } else if (type === 'ema') {
1471
+ data = calculateEMA(window.currentCandles, { period: config.value });
1472
+ if (data && data.length > 0) {
1473
+ const series = window.chart.addLineSeries({
1474
+ color: config.color,
1475
+ lineWidth: config.thickness,
1476
+ title: `EMA ${config.value}`,
1477
+ });
1478
+ series.setData(data);
1479
+ window.dynamicIndicatorSeries.push(series);
1480
+ }
1481
+ } else if (type === 'rsi') {
1482
+ data = calculateRSI(window.currentCandles, { period: config.value });
1483
+ if (data && data.length > 0) {
1484
+ const series = window.chart.addLineSeries({
1485
+ color: config.color,
1486
+ lineWidth: config.thickness,
1487
+ title: `RSI ${config.value}`,
1488
+ priceScaleId: 'rsi',
1489
+ });
1490
+ series.priceScale().applyOptions({
1491
+ scaleMargins: { top: 0.8, bottom: 0 },
1492
+ });
1493
+ series.setData(data);
1494
+ window.dynamicIndicatorSeries.push(series);
1495
+ }
1496
+ } else if (type === 'macd') {
1497
+ // MACD 파라미터와 색상을 UI에서 읽기
1498
+ const fastField = document.querySelector('.macd-fast-field');
1499
+ const slowField = document.querySelector('.macd-slow-field');
1500
+ const signalField = document.querySelector('.macd-signal-field');
1501
+ const macdColorPicker = document.querySelector('.macd-line-color-picker');
1502
+ const signalColorPicker = document.querySelector('.macd-signal-color-picker');
1503
+
1504
+ const fastPeriod = fastField ? parseInt(fastField.value) || 12 : 12;
1505
+ const slowPeriod = slowField ? parseInt(slowField.value) || 26 : 26;
1506
+ const signalPeriod = signalField ? parseInt(signalField.value) || 9 : 9;
1507
+ const macdColor = macdColorPicker ? macdColorPicker.style.background : '#2962FF';
1508
+ const signalColor = signalColorPicker ? signalColorPicker.style.background : '#FF6D00';
1509
+
1510
+ const macdData = calculateMACD(window.currentCandles, { fastPeriod, slowPeriod, signalPeriod });
1511
+ if (macdData.macd && macdData.macd.length > 0) {
1512
+ const macdSeries = window.chart.addLineSeries({
1513
+ color: macdColor,
1514
+ lineWidth: config.thickness,
1515
+ title: 'MACD',
1516
+ priceScaleId: 'macd',
1517
+ });
1518
+ macdSeries.priceScale().applyOptions({
1519
+ scaleMargins: { top: 0.8, bottom: 0 },
1520
+ });
1521
+ macdSeries.setData(macdData.macd);
1522
+ window.dynamicIndicatorSeries.push(macdSeries);
1523
+
1524
+ // Signal line
1525
+ const signalSeries = window.chart.addLineSeries({
1526
+ color: signalColor,
1527
+ lineWidth: config.thickness,
1528
+ title: 'Signal',
1529
+ priceScaleId: 'macd',
1530
+ });
1531
+ signalSeries.setData(macdData.signal);
1532
+ window.dynamicIndicatorSeries.push(signalSeries);
1533
+
1534
+ // Histogram
1535
+ const histSeries = window.chart.addHistogramSeries({
1536
+ color: '#26a69a',
1537
+ priceScaleId: 'macd',
1538
+ });
1539
+ histSeries.setData(macdData.histogram);
1540
+ window.dynamicIndicatorSeries.push(histSeries);
1541
+ }
1542
+ } else if (type === 'bbands') {
1543
+ const stdDev = config.stdDev || 2;
1544
+ const bbData = calculateBollingerBands(window.currentCandles, { period: config.value, stdDev });
1545
+ if (bbData.upper && bbData.upper.length > 0) {
1546
+ // 볼린저 밴드는 각 라인마다 다른 색상 사용
1547
+ const upperColor = config.upperColor || '#F23645'; // 빨강
1548
+ const middleColor = config.middleColor || '#2962FF'; // 파랑
1549
+ const lowerColor = config.lowerColor || '#089981'; // 초록
1550
+
1551
+ const upperSeries = window.chart.addLineSeries({
1552
+ color: upperColor,
1553
+ lineWidth: config.thickness,
1554
+ title: 'BB Upper',
1555
+ });
1556
+ upperSeries.setData(bbData.upper);
1557
+ window.dynamicIndicatorSeries.push(upperSeries);
1558
+
1559
+ const middleSeries = window.chart.addLineSeries({
1560
+ color: middleColor,
1561
+ lineWidth: config.thickness,
1562
+ title: 'BB Middle',
1563
+ });
1564
+ middleSeries.setData(bbData.middle);
1565
+ window.dynamicIndicatorSeries.push(middleSeries);
1566
+
1567
+ const lowerSeries = window.chart.addLineSeries({
1568
+ color: lowerColor,
1569
+ lineWidth: config.thickness,
1570
+ title: 'BB Lower',
1571
+ });
1572
+ lowerSeries.setData(bbData.lower);
1573
+ window.dynamicIndicatorSeries.push(lowerSeries);
1574
+ }
1575
+ }
1576
+ });
1577
+ });
1578
+ }
1579
+
1580
+ // ===== Custom Text Modal Functionality =====
1581
+ // ===== Custom Text Modal Functionality =====
1582
+ const textModal = document.getElementById('textModal');
1583
+ const textInput = document.getElementById('textInput');
1584
+ const textModalConfirm = document.getElementById('textModalConfirm');
1585
+ const textModalCancel = document.getElementById('textModalCancel');
1586
+
1587
+ let textModalResolve = null;
1588
+
1589
+ function showTextModal(defaultValue = '') {
1590
+ return new Promise((resolve) => {
1591
+ textModalResolve = resolve;
1592
+ textInput.value = defaultValue;
1593
+ textModal.style.display = 'flex';
1594
+ setTimeout(() => {
1595
+ textInput.focus();
1596
+ textInput.select();
1597
+ }, 100);
1598
+ });
1599
+ }
1600
+
1601
+ function hideTextModal() {
1602
+ textModal.style.display = 'none';
1603
+ textInput.value = '';
1604
+ }
1605
+
1606
+ // Confirm button
1607
+ textModalConfirm.addEventListener('click', () => {
1608
+ const value = textInput.value;
1609
+ hideTextModal();
1610
+ if (textModalResolve) {
1611
+ textModalResolve(value);
1612
+ textModalResolve = null;
1613
+ }
1614
+ });
1615
+
1616
+ // Cancel button
1617
+ textModalCancel.addEventListener('click', () => {
1618
+ hideTextModal();
1619
+ if (textModalResolve) {
1620
+ textModalResolve(null);
1621
+ textModalResolve = null;
1622
+ }
1623
+ });
1624
+
1625
+ // Enter key to confirm
1626
+ textInput.addEventListener('keydown', (e) => {
1627
+ if (e.key === 'Enter') {
1628
+ e.preventDefault();
1629
+ textModalConfirm.click();
1630
+ } else if (e.key === 'Escape') {
1631
+ e.preventDefault();
1632
+ textModalCancel.click();
1633
+ }
1634
+ });
1635
+
1636
+ // Close modal when clicking overlay
1637
+ textModal.addEventListener('click', (e) => {
1638
+ if (e.target === textModal) {
1639
+ textModalCancel.click();
1640
+ }
1641
+ });