{ "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 } } }