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:
156
n8n/economic-calendar-combined.json
Normal file
156
n8n/economic-calendar-combined.json
Normal file
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"name": "Economic Calendar (Combined)",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [
|
||||
{
|
||||
"field": "cronExpression",
|
||||
"expression": "0 3 * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "schedule-1",
|
||||
"name": "Schedule Trigger",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [250, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "GET",
|
||||
"path": "economic-calendar",
|
||||
"authentication": "headerAuth",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "webhook-1",
|
||||
"name": "API Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [250, 400],
|
||||
"webhookId": "economic-calendar",
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "economic-calendar-api-key",
|
||||
"name": "Economic Calendar API Key"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "https://nfs.faireconomy.media/ff_calendar_thisweek.json",
|
||||
"options": {}
|
||||
},
|
||||
"id": "http-1",
|
||||
"name": "Fetch Forex Factory",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [470, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Forex Factory 데이터 변환 및 DB 저장\nconst rawData = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\nconst newEvents = [];\n\nconst countryMap = {\n 'USD': 'US',\n 'JPY': 'JP',\n 'CNY': 'CN'\n};\n\nconst importanceMap = {\n 'Low': 1,\n 'Medium': 2,\n 'High': 3\n};\n\nconst getLocalDateStr = (d) => {\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, '0');\n const day = String(d.getDate()).padStart(2, '0');\n return `${y}-${m}-${day}`;\n};\n\nconst convertToKST = (dateStr) => {\n const d = new Date(dateStr);\n // UTC 기준으로 KST(+9) 변환 - 서버 timezone 무관\n const kstTime = new Date(d.getTime() + 9 * 60 * 60 * 1000);\n return {\n date: `${kstTime.getUTCFullYear()}-${String(kstTime.getUTCMonth() + 1).padStart(2, '0')}-${String(kstTime.getUTCDate()).padStart(2, '0')}`,\n time: `${String(kstTime.getUTCHours()).padStart(2, '0')}:${String(kstTime.getUTCMinutes()).padStart(2, '0')}`\n };\n};\n\nfor (const item of rawData) {\n const event = item.json;\n const country = countryMap[event.country];\n \n if (!country || !['US', 'JP', 'CN'].includes(country)) continue;\n if (event.impact === 'Low') continue;\n \n const kst = convertToKST(event.date);\n \n newEvents.push({\n date: kst.date,\n time: kst.time,\n country: country,\n name: event.title,\n importance: importanceMap[event.impact] || 2,\n forecast: event.forecast || '-',\n previous: event.previous || '-',\n actual: event.actual || null\n });\n}\n\nnewEvents.sort((a, b) => {\n const dateCompare = a.date.localeCompare(b.date);\n if (dateCompare !== 0) return dateCompare;\n return a.time.localeCompare(b.time);\n});\n\n// Static Data에 저장\nstaticData.events = newEvents;\nstaticData.updated = new Date().toISOString();\nstaticData.count = newEvents.length;\n\nreturn [{\n json: {\n success: true,\n message: 'Data saved to n8n database',\n count: newEvents.length,\n updated: staticData.updated\n }\n}];"
|
||||
},
|
||||
"id": "code-save",
|
||||
"name": "Transform & Save",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [690, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Static Data에서 이벤트 읽기 (없으면 직접 fetch)\nconst staticData = $getWorkflowStaticData('global');\n\n// 캐시 유효 시간 (6시간)\nconst CACHE_TTL = 6 * 60 * 60 * 1000;\nconst now = Date.now();\nconst lastUpdated = staticData.updated ? new Date(staticData.updated).getTime() : 0;\nconst isCacheValid = staticData.events?.length > 0 && (now - lastUpdated) < CACHE_TTL;\n\nif (isCacheValid) {\n return [{ json: {\n data: staticData.events,\n source: 'forexfactory',\n updated: staticData.updated,\n count: staticData.count,\n cached: true\n }}];\n}\n\n// 캐시가 없거나 만료된 경우 직접 fetch (n8n $http 사용)\nconst response = await this.helpers.httpRequest({\n method: 'GET',\n url: 'https://nfs.faireconomy.media/ff_calendar_thisweek.json',\n json: true\n});\nconst rawData = response;\n\nconst countryMap = { 'USD': 'US', 'JPY': 'JP', 'CNY': 'CN' };\nconst importanceMap = { 'Low': 1, 'Medium': 2, 'High': 3 };\n\nconst getLocalDateStr = (d) => {\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, '0');\n const day = String(d.getDate()).padStart(2, '0');\n return `${y}-${m}-${day}`;\n};\n\nconst convertToKST = (dateStr) => {\n const d = new Date(dateStr);\n // UTC 기준으로 KST(+9) 변환 - 서버 timezone 무관\n const kstTime = new Date(d.getTime() + 9 * 60 * 60 * 1000);\n return {\n date: `${kstTime.getUTCFullYear()}-${String(kstTime.getUTCMonth() + 1).padStart(2, '0')}-${String(kstTime.getUTCDate()).padStart(2, '0')}`,\n time: `${String(kstTime.getUTCHours()).padStart(2, '0')}:${String(kstTime.getUTCMinutes()).padStart(2, '0')}`\n };\n};\n\nconst newEvents = [];\nfor (const event of rawData) {\n const country = countryMap[event.country];\n if (!country || !['US', 'JP', 'CN'].includes(country)) continue;\n if (event.impact === 'Low') continue;\n \n const kst = convertToKST(event.date);\n newEvents.push({\n date: kst.date,\n time: kst.time,\n country: country,\n name: event.title,\n importance: importanceMap[event.impact] || 2,\n forecast: event.forecast || '-',\n previous: event.previous || '-',\n actual: event.actual || null\n });\n}\n\nnewEvents.sort((a, b) => {\n const dateCompare = a.date.localeCompare(b.date);\n if (dateCompare !== 0) return dateCompare;\n return a.time.localeCompare(b.time);\n});\n\n// 캐시에 저장\nstaticData.events = newEvents;\nstaticData.updated = new Date().toISOString();\nstaticData.count = newEvents.length;\n\nreturn [{ json: {\n data: newEvents,\n source: 'forexfactory',\n updated: staticData.updated,\n count: newEvents.length,\n cached: false\n}}];"
|
||||
},
|
||||
"id": "code-read",
|
||||
"name": "Read Data",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [470, 400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {
|
||||
"responseHeaders": {
|
||||
"entries": [
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
},
|
||||
{
|
||||
"name": "Cache-Control",
|
||||
"value": "max-age=300"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "respond-1",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [690, 400]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Schedule Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Fetch Forex Factory",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"API Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Read Data",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Fetch Forex Factory": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Transform & Save",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Read Data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"staticData": {
|
||||
"global": {
|
||||
"events": [],
|
||||
"updated": null,
|
||||
"count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user