Add economic calendar feature with n8n integration
All checks were successful
Deploy to Server / deploy (push) Successful in 36s
All checks were successful
Deploy to Server / deploy (push) Successful in 36s
- Add economic calendar tab with monthly view - Display today's events in header - Add weekly summary section - Integrate with Forex Factory via n8n webhook - Add Header Auth API authentication - Add KST timezone conversion - Add country filter (US, JP, CN) - Add importance-based event styling - Add more events modal for days with many events - Update calendar grid to show up to 4 events per day - Add n8n workflow configuration files
This commit is contained in:
621
app.js
621
app.js
@@ -1057,7 +1057,628 @@ class TradingView {
|
||||
}
|
||||
}
|
||||
|
||||
// Economic Calendar Class
|
||||
class EconomicCalendar {
|
||||
constructor() {
|
||||
this.events = [];
|
||||
this.filteredEvents = [];
|
||||
this.selectedCountry = 'all';
|
||||
this.selectedPeriod = 'week';
|
||||
this.currentMonth = new Date();
|
||||
|
||||
// 데이터 소스 설정:
|
||||
// - 로컬 JSON 파일: 'data/economic-calendar.json'
|
||||
// - n8n webhook: 'https://n8n.binibini.dedyn.io/webhook/economic-calendar'
|
||||
this.dataUrl = 'https://n8n.binibini.dedyn.io/webhook/economic-calendar';
|
||||
|
||||
// API 인증 키 (n8n Header Auth와 동일한 값 사용)
|
||||
// 키 생성: openssl rand -hex 32
|
||||
this.apiKey = 'd46c4eb3b8ce1864c4994c45db11f80f1732fecd3caebd56518f5f6ce2be205e';
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.setupEventListeners();
|
||||
await this.loadCalendarData();
|
||||
this.renderHeaderEvents();
|
||||
this.renderWeeklySummary();
|
||||
this.renderCalendarList();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Tab navigation
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const tab = e.target.dataset.tab;
|
||||
this.switchTab(tab);
|
||||
});
|
||||
});
|
||||
|
||||
// Country filter buttons
|
||||
document.querySelectorAll('.country-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
document.querySelectorAll('.country-btn').forEach(b => b.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
this.selectedCountry = e.target.dataset.country;
|
||||
this.renderCalendarList();
|
||||
});
|
||||
});
|
||||
|
||||
// Period filter
|
||||
document.getElementById('calendar-period')?.addEventListener('change', (e) => {
|
||||
this.selectedPeriod = e.target.value;
|
||||
this.renderCalendarList();
|
||||
});
|
||||
|
||||
// "More" button to switch to calendar tab
|
||||
document.getElementById('show-calendar-btn')?.addEventListener('click', () => {
|
||||
this.switchTab('calendar');
|
||||
});
|
||||
}
|
||||
|
||||
switchTab(tab) {
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tab);
|
||||
});
|
||||
|
||||
// Update tab content
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.toggle('active', content.id === `${tab}-tab`);
|
||||
});
|
||||
|
||||
// Show/hide timeframe selector
|
||||
const timeframeSelect = document.getElementById('timeframe');
|
||||
if (timeframeSelect) {
|
||||
timeframeSelect.style.display = tab === 'chart' ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async loadCalendarData() {
|
||||
try {
|
||||
const headers = {};
|
||||
// API 키가 설정된 경우 인증 헤더 추가
|
||||
if (this.apiKey && this.apiKey !== 'YOUR_API_KEY_HERE') {
|
||||
headers['X-API-Key'] = this.apiKey;
|
||||
}
|
||||
|
||||
const response = await fetch(this.dataUrl, { headers });
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// n8n webhook returns {data: [...]}, local JSON returns [...]
|
||||
this.events = Array.isArray(data) ? data : (data.data || []);
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
console.warn('API 인증 실패: API 키를 확인하세요');
|
||||
this.events = this.getSampleData();
|
||||
} else {
|
||||
// Use sample data if file doesn't exist
|
||||
this.events = this.getSampleData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Using sample calendar data');
|
||||
this.events = this.getSampleData();
|
||||
}
|
||||
}
|
||||
|
||||
getSampleData() {
|
||||
const today = new Date();
|
||||
// 로컬 시간 기준 YYYY-MM-DD 형식 반환 (UTC 변환으로 인한 날짜 밀림 방지)
|
||||
const getDateStr = (daysOffset) => {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() + daysOffset);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
date: getDateStr(0),
|
||||
time: '09:00',
|
||||
country: 'CN',
|
||||
name: '제조업 PMI',
|
||||
importance: 3,
|
||||
forecast: '50.2',
|
||||
previous: '50.3',
|
||||
actual: null
|
||||
},
|
||||
{
|
||||
date: getDateStr(0),
|
||||
time: '23:00',
|
||||
country: 'US',
|
||||
name: 'CB 소비자신뢰지수',
|
||||
importance: 3,
|
||||
forecast: '112.0',
|
||||
previous: '111.7',
|
||||
actual: null
|
||||
},
|
||||
{
|
||||
date: getDateStr(1),
|
||||
time: '08:30',
|
||||
country: 'JP',
|
||||
name: '실업률',
|
||||
importance: 2,
|
||||
forecast: '2.5%',
|
||||
previous: '2.5%',
|
||||
actual: null
|
||||
},
|
||||
{
|
||||
date: getDateStr(1),
|
||||
time: '10:00',
|
||||
country: 'KR',
|
||||
name: '산업생산 (MoM)',
|
||||
importance: 2,
|
||||
forecast: '0.3%',
|
||||
previous: '-0.3%',
|
||||
actual: null
|
||||
},
|
||||
{
|
||||
date: getDateStr(2),
|
||||
time: '22:00',
|
||||
country: 'US',
|
||||
name: 'ISM 제조업 PMI',
|
||||
importance: 3,
|
||||
forecast: '48.0',
|
||||
previous: '46.7',
|
||||
actual: null
|
||||
},
|
||||
{
|
||||
date: getDateStr(3),
|
||||
time: '04:00',
|
||||
country: 'US',
|
||||
name: 'FOMC 회의록',
|
||||
importance: 3,
|
||||
forecast: '-',
|
||||
previous: '-',
|
||||
actual: null
|
||||
},
|
||||
{
|
||||
date: getDateStr(4),
|
||||
time: '09:30',
|
||||
country: 'CN',
|
||||
name: 'Caixin 서비스업 PMI',
|
||||
importance: 2,
|
||||
forecast: '51.5',
|
||||
previous: '51.5',
|
||||
actual: null
|
||||
},
|
||||
{
|
||||
date: getDateStr(4),
|
||||
time: '22:30',
|
||||
country: 'US',
|
||||
name: '비농업 고용지수 (NFP)',
|
||||
importance: 3,
|
||||
forecast: '180K',
|
||||
previous: '227K',
|
||||
actual: null
|
||||
},
|
||||
{
|
||||
date: getDateStr(5),
|
||||
time: '08:00',
|
||||
country: 'KR',
|
||||
name: 'CPI (YoY)',
|
||||
importance: 3,
|
||||
forecast: '1.8%',
|
||||
previous: '1.5%',
|
||||
actual: null
|
||||
},
|
||||
{
|
||||
date: getDateStr(6),
|
||||
time: '10:30',
|
||||
country: 'JP',
|
||||
name: '경상수지',
|
||||
importance: 2,
|
||||
forecast: '2.5T',
|
||||
previous: '2.3T',
|
||||
actual: null
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getCountryFlag(countryCode) {
|
||||
const flags = {
|
||||
'US': '🇺🇸',
|
||||
'KR': '🇰🇷',
|
||||
'CN': '🇨🇳',
|
||||
'JP': '🇯🇵'
|
||||
};
|
||||
return flags[countryCode] || '🌐';
|
||||
}
|
||||
|
||||
getFilteredEvents() {
|
||||
let filtered = [...this.events];
|
||||
|
||||
// Filter by country
|
||||
if (this.selectedCountry !== 'all') {
|
||||
filtered = filtered.filter(e => e.country === this.selectedCountry);
|
||||
}
|
||||
|
||||
// Filter by period
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (this.selectedPeriod === 'week') {
|
||||
const weekEnd = new Date(today);
|
||||
weekEnd.setDate(weekEnd.getDate() + 7);
|
||||
filtered = filtered.filter(e => {
|
||||
const eventDate = new Date(e.date);
|
||||
return eventDate >= today && eventDate < weekEnd;
|
||||
});
|
||||
} else if (this.selectedPeriod === 'month') {
|
||||
const monthEnd = new Date(today);
|
||||
monthEnd.setMonth(monthEnd.getMonth() + 1);
|
||||
filtered = filtered.filter(e => {
|
||||
const eventDate = new Date(e.date);
|
||||
return eventDate >= today && eventDate < monthEnd;
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by date and time
|
||||
filtered.sort((a, b) => {
|
||||
const dateCompare = a.date.localeCompare(b.date);
|
||||
if (dateCompare !== 0) return dateCompare;
|
||||
return a.time.localeCompare(b.time);
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
renderHeaderEvents() {
|
||||
const container = document.getElementById('header-events');
|
||||
if (!container) return;
|
||||
|
||||
// Get today's date string (로컬 시간 기준)
|
||||
const today = new Date();
|
||||
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
|
||||
// Filter today's events (importance === 3 only)
|
||||
const todayEvents = this.events
|
||||
.filter(e => e.date === todayStr && e.importance === 3)
|
||||
.sort((a, b) => a.time.localeCompare(b.time))
|
||||
.slice(0, 4); // Maximum 4 events in header
|
||||
|
||||
if (todayEvents.length === 0) {
|
||||
container.innerHTML = '<span class="header-events-empty">오늘 주요 일정 없음</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = todayEvents.map(event => {
|
||||
const stars = '★'.repeat(event.importance);
|
||||
return `
|
||||
<div class="header-event-item importance-${event.importance}" title="${event.name} - ${event.forecast || '-'}">
|
||||
<span class="header-event-time">${event.time}</span>
|
||||
<span class="header-event-country">${this.getCountryFlag(event.country)}</span>
|
||||
<span class="header-event-name">${event.name}</span>
|
||||
<span class="header-event-stars">${stars}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
renderWeeklySummary() {
|
||||
const container = document.getElementById('weekly-events');
|
||||
if (!container) return;
|
||||
|
||||
const weekEvents = this.events
|
||||
.filter(e => {
|
||||
const eventDate = new Date(e.date);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const weekEnd = new Date(today);
|
||||
weekEnd.setDate(weekEnd.getDate() + 7);
|
||||
return eventDate >= today && eventDate < weekEnd && e.importance >= 2;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const dateCompare = a.date.localeCompare(b.date);
|
||||
if (dateCompare !== 0) return dateCompare;
|
||||
return a.time.localeCompare(b.time);
|
||||
})
|
||||
.slice(0, 8);
|
||||
|
||||
if (weekEvents.length === 0) {
|
||||
container.innerHTML = '<span class="loading-text">이번주 주요 일정이 없습니다</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = weekEvents.map(event => {
|
||||
const eventDate = new Date(event.date);
|
||||
const dateStr = `${eventDate.getMonth() + 1}/${eventDate.getDate()}`;
|
||||
const stars = '★'.repeat(event.importance);
|
||||
|
||||
return `
|
||||
<div class="weekly-event-item">
|
||||
<span class="weekly-event-date">${dateStr}</span>
|
||||
<span class="weekly-event-country">${this.getCountryFlag(event.country)}</span>
|
||||
<span class="weekly-event-name">${event.name}</span>
|
||||
<span class="weekly-event-importance">${stars}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
renderCalendarList() {
|
||||
const container = document.getElementById('calendar-list');
|
||||
if (!container) return;
|
||||
|
||||
// 월간 선택 시 캘린더 그리드로 표시
|
||||
if (this.selectedPeriod === 'month') {
|
||||
this.renderMonthlyCalendar(container);
|
||||
return;
|
||||
}
|
||||
|
||||
// 주간 선택 시 리스트로 표시
|
||||
const events = this.getFilteredEvents();
|
||||
|
||||
if (events.length === 0) {
|
||||
container.innerHTML = '<div class="loading-text">해당 기간에 일정이 없습니다</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group events by date
|
||||
const grouped = events.reduce((acc, event) => {
|
||||
if (!acc[event.date]) {
|
||||
acc[event.date] = [];
|
||||
}
|
||||
acc[event.date].push(event);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const html = Object.entries(grouped).map(([date, dayEvents]) => {
|
||||
const eventDate = new Date(date);
|
||||
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const dateStr = `${eventDate.getMonth() + 1}월 ${eventDate.getDate()}일 (${dayNames[eventDate.getDay()]})`;
|
||||
|
||||
const eventsHtml = dayEvents.map(event => {
|
||||
const stars = [1, 2, 3].map(i =>
|
||||
`<span class="importance-star ${i <= event.importance ? 'active' : ''}">★</span>`
|
||||
).join('');
|
||||
|
||||
return `
|
||||
<div class="calendar-event">
|
||||
<span class="event-time">${event.time}</span>
|
||||
<span class="event-country">${this.getCountryFlag(event.country)}</span>
|
||||
<div class="event-info">
|
||||
<div class="event-name">${event.name}</div>
|
||||
</div>
|
||||
<div class="event-importance">${stars}</div>
|
||||
<div class="event-values">
|
||||
<div class="event-value">
|
||||
<div class="event-value-label">예상</div>
|
||||
<div class="event-value-number">${event.forecast || '-'}</div>
|
||||
</div>
|
||||
<div class="event-value">
|
||||
<div class="event-value-label">이전</div>
|
||||
<div class="event-value-number">${event.previous || '-'}</div>
|
||||
</div>
|
||||
<div class="event-value">
|
||||
<div class="event-value-label">실제</div>
|
||||
<div class="event-value-number ${event.actual ? (parseFloat(event.actual) > parseFloat(event.forecast) ? 'positive' : 'negative') : ''}">${event.actual || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="calendar-date-group">
|
||||
<div class="calendar-date-header">${dateStr}</div>
|
||||
${eventsHtml}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
renderMonthlyCalendar(container) {
|
||||
const year = this.currentMonth.getFullYear();
|
||||
const month = this.currentMonth.getMonth();
|
||||
|
||||
// 해당 월의 첫째 날과 마지막 날
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
|
||||
// 캘린더 시작일 (해당 월 첫째 날이 속한 주의 일요일)
|
||||
const startDate = new Date(firstDay);
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay());
|
||||
|
||||
// 캘린더 종료일 (해당 월 마지막 날이 속한 주의 토요일)
|
||||
const endDate = new Date(lastDay);
|
||||
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()));
|
||||
|
||||
// 이벤트를 날짜별로 그룹화
|
||||
const eventsByDate = this.getMonthEvents(year, month);
|
||||
|
||||
// 오늘 날짜
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
// 월 제목
|
||||
const monthTitle = `${year}년 ${month + 1}월`;
|
||||
|
||||
// 요일 헤더
|
||||
const weekdays = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
|
||||
// 날짜 셀 생성
|
||||
let daysHtml = '';
|
||||
const currentDate = new Date(startDate);
|
||||
|
||||
// 로컬 시간 기준 YYYY-MM-DD 형식 반환
|
||||
const getLocalDateStr = (date) => {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
};
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
const dateStr = getLocalDateStr(currentDate);
|
||||
const isOtherMonth = currentDate.getMonth() !== month;
|
||||
const isToday = currentDate.getTime() === today.getTime();
|
||||
const dayEvents = eventsByDate[dateStr] || [];
|
||||
|
||||
// 국가 필터 적용
|
||||
const filteredDayEvents = this.selectedCountry === 'all'
|
||||
? dayEvents
|
||||
: dayEvents.filter(e => e.country === this.selectedCountry);
|
||||
|
||||
// 최대 4개까지만 표시
|
||||
const visibleEvents = filteredDayEvents.slice(0, 4);
|
||||
const moreCount = filteredDayEvents.length - 4;
|
||||
|
||||
const eventsHtml = visibleEvents.map(event => {
|
||||
const stars = '★'.repeat(event.importance);
|
||||
return `
|
||||
<div class="day-event importance-${event.importance}" title="${event.time} ${event.name}">
|
||||
<span class="day-event-time">${event.time}</span>
|
||||
<span class="day-event-country">${this.getCountryFlag(event.country)}</span>
|
||||
<span class="day-event-name">${event.name}</span>
|
||||
<span class="day-event-stars">${stars}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const moreHtml = moreCount > 0 ? `<div class="day-event-more" data-date="${dateStr}">+${moreCount}개 더보기</div>` : '';
|
||||
|
||||
daysHtml += `
|
||||
<div class="calendar-day ${isOtherMonth ? 'other-month' : ''} ${isToday ? 'today' : ''}">
|
||||
<div class="day-number">${currentDate.getDate()}</div>
|
||||
<div class="day-events">
|
||||
${eventsHtml}
|
||||
${moreHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="calendar-grid">
|
||||
<div class="calendar-grid-header">
|
||||
<div class="calendar-nav">
|
||||
<button class="calendar-nav-btn" id="prev-month">< 이전</button>
|
||||
</div>
|
||||
<div class="calendar-month-title">${monthTitle}</div>
|
||||
<div class="calendar-nav">
|
||||
<button class="calendar-nav-btn" id="next-month">다음 ></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-weekdays">
|
||||
${weekdays.map(day => `<div class="calendar-weekday">${day}</div>`).join('')}
|
||||
</div>
|
||||
<div class="calendar-days">
|
||||
${daysHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 네비게이션 버튼 이벤트
|
||||
document.getElementById('prev-month')?.addEventListener('click', () => {
|
||||
this.currentMonth.setMonth(this.currentMonth.getMonth() - 1);
|
||||
this.renderCalendarList();
|
||||
});
|
||||
|
||||
document.getElementById('next-month')?.addEventListener('click', () => {
|
||||
this.currentMonth.setMonth(this.currentMonth.getMonth() + 1);
|
||||
this.renderCalendarList();
|
||||
});
|
||||
|
||||
// 더보기 버튼 클릭 이벤트
|
||||
document.querySelectorAll('.day-event-more').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const dateStr = e.target.dataset.date;
|
||||
this.showDayEventsModal(dateStr, eventsByDate[dateStr] || []);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
showDayEventsModal(dateStr, events) {
|
||||
// 국가 필터 적용
|
||||
const filteredEvents = this.selectedCountry === 'all'
|
||||
? events
|
||||
: events.filter(e => e.country === this.selectedCountry);
|
||||
|
||||
const date = new Date(dateStr);
|
||||
const dateTitle = `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`;
|
||||
|
||||
const eventsHtml = filteredEvents.map(event => {
|
||||
const stars = '★'.repeat(event.importance);
|
||||
return `
|
||||
<div class="modal-event importance-${event.importance}">
|
||||
<span class="modal-event-time">${event.time}</span>
|
||||
<span class="modal-event-country">${this.getCountryFlag(event.country)}</span>
|
||||
<span class="modal-event-name">${event.name}</span>
|
||||
<span class="modal-event-stars">${stars}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// 모달 생성
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'events-modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="events-modal">
|
||||
<div class="events-modal-header">
|
||||
<h3>${dateTitle} 일정</h3>
|
||||
<button class="events-modal-close">×</button>
|
||||
</div>
|
||||
<div class="events-modal-body">
|
||||
${eventsHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// 닫기 이벤트
|
||||
modal.querySelector('.events-modal-close').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getMonthEvents(year, month) {
|
||||
const grouped = {};
|
||||
|
||||
this.events.forEach(event => {
|
||||
const eventDate = new Date(event.date);
|
||||
// 해당 월 전후 1주일까지 포함 (캘린더에 표시되는 범위)
|
||||
const eventYear = eventDate.getFullYear();
|
||||
const eventMonth = eventDate.getMonth();
|
||||
|
||||
// 해당 월 또는 인접 월의 이벤트만 포함
|
||||
if ((eventYear === year && eventMonth === month) ||
|
||||
(eventYear === year && eventMonth === month - 1) ||
|
||||
(eventYear === year && eventMonth === month + 1) ||
|
||||
(eventYear === year - 1 && month === 0 && eventMonth === 11) ||
|
||||
(eventYear === year + 1 && month === 11 && eventMonth === 0)) {
|
||||
|
||||
if (!grouped[event.date]) {
|
||||
grouped[event.date] = [];
|
||||
}
|
||||
grouped[event.date].push(event);
|
||||
}
|
||||
});
|
||||
|
||||
// 각 날짜의 이벤트를 시간순으로 정렬
|
||||
Object.keys(grouped).forEach(date => {
|
||||
grouped[date].sort((a, b) => a.time.localeCompare(b.time));
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the application
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.tradingView = new TradingView();
|
||||
window.economicCalendar = new EconomicCalendar();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user