The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Firestore \uc815\uc624 \uc694\uc57d \ub9ac\ud3ec\ud2b8 (Gemini)",
"nodes": [
{
"parameters": {
"triggerTimes": {
"item": [
{
"mode": "everyDay",
"hour": 12,
"minute": 0
}
]
},
"timezone": "Asia/Seoul"
},
"id": "cron-trigger",
"name": "Cron (\ub9e4\uc77c 12:00 KST)",
"type": "n8n-nodes-base.cron",
"typeVersion": 1,
"position": [
260,
260
]
},
{
"parameters": {
"functionCode": "// \uc624\ub298 00:00 KST ~ \ud604\uc7ac(\uc815\uc624) \uc2dc\uac04 \ubc94\uc704 \uacc4\uc0b0\nconst now = new Date();\nconst y = now.getFullYear();\nconst m = now.getMonth();\nconst d = now.getDate();\nconst start = new Date(y, m, d, 0, 0, 0);\nconst end = now;\n\n// \uc5b4\uc81c \uac19\uc740 \uc2dc\uac04\ub300\ub3c4 \uacc4\uc0b0 (\ube44\uad50\uc6a9)\nconst yesterday = new Date(start);\nyesterday.setDate(yesterday.getDate() - 1);\nconst yesterdayEnd = new Date(end);\nyesterdayEnd.setDate(yesterdayEnd.getDate() - 1);\n\nreturn [{\n json: {\n startISO: start.toISOString(),\n endISO: end.toISOString(),\n yesterdayStartISO: yesterday.toISOString(),\n yesterdayEndISO: yesterdayEnd.toISOString(),\n dateRange: `${start.toISOString().split('T')[0]} 00:00 ~ ${end.toISOString().split('T')[0]} 12:00`\n }\n}];"
},
"id": "time-calculator",
"name": "\uc2dc\uac04 \ubc94\uc704 \uacc4\uc0b0",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
520,
260
]
},
{
"parameters": {
"operation": "getAll",
"projectId": "what2eat-e21e3",
"collection": "reports",
"query": {
"queryFieldsUi": {
"queryFieldsValues": [
{
"field": "created_at",
"operator": ">=",
"value": "={{$json.startISO}}"
},
{
"field": "created_at",
"operator": "<=",
"value": "={{$json.endISO}}"
}
]
}
},
"limit": 1000
},
"id": "firestore-today",
"name": "Firestore: \uc624\ub298 \ub370\uc774\ud130 \uc870\ud68c",
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
"typeVersion": 1,
"credentials": {
"googleFirebaseCloudFirestoreOAuth2Api": {
"name": "<your credential>"
}
},
"position": [
780,
200
]
},
{
"parameters": {
"operation": "getAll",
"projectId": "what2eat-e21e3",
"collection": "reports",
"query": {
"queryFieldsUi": {
"queryFieldsValues": [
{
"field": "created_at",
"operator": ">=",
"value": "={{$json.yesterdayStartISO}}"
},
{
"field": "created_at",
"operator": "<=",
"value": "={{$json.yesterdayEndISO}}"
}
]
}
},
"limit": 1000
},
"id": "firestore-yesterday",
"name": "Firestore: \uc5b4\uc81c \ub370\uc774\ud130 \uc870\ud68c",
"type": "n8n-nodes-base.googleFirebaseCloudFirestore",
"typeVersion": 1,
"credentials": {
"googleFirebaseCloudFirestoreOAuth2Api": {
"name": "<your credential>"
}
},
"position": [
780,
320
]
},
{
"parameters": {
"functionCode": "// \uc624\ub298\uacfc \uc5b4\uc81c \ub370\uc774\ud130\ub97c \uc9d1\uacc4\ud558\uace0 \ube44\uad50 \ubd84\uc11d\uc6a9 \ud14d\uc2a4\ud2b8 \uc0dd\uc131\nconst todayData = $input.first().json;\nconst yesterdayData = $input.last().json;\n\n// \uc624\ub298 \ub370\uc774\ud130 \uc9d1\uacc4\nconst todayItems = todayData || [];\nconst todayByCategory = {};\nlet todayTotalValue = 0;\n\nfor (const item of todayItems) {\n const category = item.category || '\uae30\ud0c0';\n const value = Number(item.value || 0);\n \n if (!todayByCategory[category]) {\n todayByCategory[category] = { count: 0, sum: 0, items: [] };\n }\n \n todayByCategory[category].count += 1;\n todayByCategory[category].sum += value;\n todayByCategory[category].items.push({\n title: item.title || '\uc81c\ubaa9 \uc5c6\uc74c',\n value: value,\n created_at: item.created_at\n });\n todayTotalValue += value;\n}\n\n// \uc5b4\uc81c \ub370\uc774\ud130 \uc9d1\uacc4\nconst yesterdayItems = yesterdayData || [];\nconst yesterdayByCategory = {};\nlet yesterdayTotalValue = 0;\n\nfor (const item of yesterdayItems) {\n const category = item.category || '\uae30\ud0c0';\n const value = Number(item.value || 0);\n \n if (!yesterdayByCategory[category]) {\n yesterdayByCategory[category] = { count: 0, sum: 0 };\n }\n \n yesterdayByCategory[category].count += 1;\n yesterdayByCategory[category].sum += value;\n yesterdayTotalValue += value;\n}\n\n// \uac01 \uce74\ud14c\uace0\ub9ac\ubcc4 \uc0c1\uc704 \ud56d\ubaa9 \uc815\ub82c\nfor (const category of Object.keys(todayByCategory)) {\n todayByCategory[category].items.sort((a, b) => b.value - a.value);\n todayByCategory[category].top5 = todayByCategory[category].items.slice(0, 5);\n}\n\n// \uc804\uccb4 \uc0c1\uc704 \ud56d\ubaa9\nconst allTodayItems = [];\nfor (const categoryInfo of Object.values(todayByCategory)) {\n allTodayItems.push(...categoryInfo.items);\n}\nallTodayItems.sort((a, b) => b.value - a.value);\n\n// \ubcc0\ud654\uc728 \uacc4\uc0b0\nconst valueChangePercent = yesterdayTotalValue > 0 ? \n ((todayTotalValue - yesterdayTotalValue) / yesterdayTotalValue * 100).toFixed(1) : 0;\n\n// Gemini\uc5d0 \uc804\ub2ec\ud560 \ubd84\uc11d \ud14d\uc2a4\ud2b8 \uc0dd\uc131\nconst analysisText = `\ud83d\udcca **Firestore \ub370\uc774\ud130 \ubd84\uc11d \uc694\uccad**\n\n**\uae30\uac04**: ${$json.dateRange}\n**\ucd1d \ubb38\uc11c \uc218**: ${todayItems.length}\uac1c (\uc5b4\uc81c: ${yesterdayItems.length}\uac1c)\n**\ucd1d \uac12**: ${todayTotalValue.toFixed(2)} (\uc5b4\uc81c: ${yesterdayTotalValue.toFixed(2)}, \ubcc0\ud654: ${valueChangePercent}%)\n\n**\uce74\ud14c\uace0\ub9ac\ubcc4 \uc9d1\uacc4 (\uc624\ub298)**:\n` +\nObject.entries(todayByCategory).map(([category, info]) => {\n const yesterdayInfo = yesterdayByCategory[category] || { count: 0, sum: 0 };\n const countChange = yesterdayInfo.count > 0 ? \n ((info.count - yesterdayInfo.count) / yesterdayInfo.count * 100).toFixed(1) : 'N/A';\n const valueChange = yesterdayInfo.sum > 0 ? \n ((info.sum - yesterdayInfo.sum) / yesterdayInfo.sum * 100).toFixed(1) : 'N/A';\n \n return `- **${category}**: \uac1c\uc218=${info.count} (${countChange}%), \ud569\uacc4=${info.sum.toFixed(2)} (${valueChange}%)\\n \uc0c1\uc7045: ${info.top5.map(item => `${item.title}(${item.value})`).join(', ')}`;\n}).join('\\n') +\n`\\n\\n**\uc804\uccb4 \uc0c1\uc704 10\uac1c \ud56d\ubaa9**:\\n` +\nallTodayItems.slice(0, 10).map((item, i) => `${i+1}. ${item.title}: ${item.value}`).join('\\n') +\n`\\n\\n**\ud2b9\uc774\uc0ac\ud56d**:\\n- \ucd1d \uac12 \ubcc0\ud654: ${valueChangePercent}%\\n- \uac00\uc7a5 \ud65c\ubc1c\ud55c \uce74\ud14c\uace0\ub9ac: ${Object.entries(todayByCategory).sort((a,b) => b[1].count - a[1].count)[0]?.[0] || 'N/A'}\\n- \ucd5c\uace0\uac12 \ud56d\ubaa9: ${allTodayItems[0]?.title || 'N/A'} (${allTodayItems[0]?.value || 0})`;\n\nreturn [{\n json: {\n analysisText,\n todayData: {\n totalCount: todayItems.length,\n totalValue: todayTotalValue,\n categories: todayByCategory,\n topItems: allTodayItems.slice(0, 10)\n },\n yesterdayData: {\n totalCount: yesterdayItems.length,\n totalValue: yesterdayTotalValue\n },\n comparison: {\n valueChangePercent: parseFloat(valueChangePercent),\n countChangePercent: yesterdayItems.length > 0 ? \n ((todayItems.length - yesterdayItems.length) / yesterdayItems.length * 100).toFixed(1) : 'N/A'\n }\n }\n}];"
},
"id": "data-aggregator",
"name": "\ub370\uc774\ud130 \uc9d1\uacc4 & \ubd84\uc11d \ud14d\uc2a4\ud2b8 \uc0dd\uc131",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
1040,
260
]
},
{
"parameters": {
"model": "gemini-pro",
"options": {
"temperature": 0.2,
"maxTokens": 2000
},
"systemMessage": "\ub2f9\uc2e0\uc740 \ub370\uc774\ud130 \ubd84\uc11d \uc804\ubb38\uac00\uc785\ub2c8\ub2e4. Firestore \ub370\uc774\ud130\ub97c \ubd84\uc11d\ud558\uc5ec \uba85\ud655\ud558\uace0 \uc2e4\ud589 \uac00\ub2a5\ud55c \uc778\uc0ac\uc774\ud2b8\uac00 \ud3ec\ud568\ub41c \ud55c\uad6d\uc5b4 \ub9ac\ud3ec\ud2b8\ub97c \uc791\uc131\ud574\uc8fc\uc138\uc694. \ub9c8\ud06c\ub2e4\uc6b4 \ud615\uc2dd\uc744 \uc0ac\uc6a9\ud558\uace0, \uad6c\uccb4\uc801\uc778 \uc218\uce58\uc640 \ubcc0\ud654\uc728\uc744 \ud3ec\ud568\ud558\uc5ec \uc804\ubb38\uc801\uc778 \ubd84\uc11d\uc744 \uc81c\uacf5\ud574\uc8fc\uc138\uc694.",
"text": "={{$json.analysisText}}\n\n\uc704 \ub370\uc774\ud130\ub97c \ubc14\ud0d5\uc73c\ub85c \ub2e4\uc74c\uc744 \ud3ec\ud568\ud55c \uc815\uc624 \uc694\uc57d \ub9ac\ud3ec\ud2b8\ub97c \uc791\uc131\ud574\uc8fc\uc138\uc694:\n1. \uc8fc\uc694 \ud2b8\ub80c\ub4dc \ubc0f \ubcc0\ud654\n2. \uce74\ud14c\uace0\ub9ac\ubcc4 \uc0c1\uc138 \ubd84\uc11d\n3. \ud2b9\uc774\uc0ac\ud56d \ubc0f \uc774\uc0c1 \uc9d5\ud6c4\n4. \uc2e4\ud589 \uac00\ub2a5\ud55c \uc778\uc0ac\uc774\ud2b8 3\uac00\uc9c0\n5. \ub0b4\uc77c\uc744 \uc704\ud55c \uad8c\uc7a5\uc0ac\ud56d\n\n\ub9c8\ud06c\ub2e4\uc6b4 \ud615\uc2dd\uc73c\ub85c \uc791\uc131\ud574\uc8fc\uc138\uc694."
},
"id": "gemini-analyzer",
"name": "Gemini AI \ubd84\uc11d",
"type": "n8n-nodes-base.googleGemini",
"typeVersion": 1,
"credentials": {
"googleGeminiApi": {
"name": "<your credential>"
}
},
"position": [
1300,
260
]
},
{
"parameters": {
"functionCode": "// \ucd5c\uc885 \ub9ac\ud3ec\ud2b8 \ud3ec\ub9f7\ud305\nconst geminiAnalysis = $json.text || $json.data || '\ubd84\uc11d \uacb0\uacfc\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.';\nconst aggregatedData = $('\ub370\uc774\ud130 \uc9d1\uacc4 & \ubd84\uc11d \ud14d\uc2a4\ud2b8 \uc0dd\uc131').first().json;\nconst timeInfo = $('\uc2dc\uac04 \ubc94\uc704 \uacc4\uc0b0').first().json;\n\nconst finalReport = `\ud83d\udcca **Firestore \uc815\uc624 \uc694\uc57d \ub9ac\ud3ec\ud2b8**\n\ud83d\udcc5 **\ubd84\uc11d \uae30\uac04**: ${timeInfo.dateRange}\n\ud83d\udcc8 **\ub370\uc774\ud130 \ud604\ud669**: \ucd1d ${aggregatedData.todayData.totalCount}\uac1c \ubb38\uc11c, \ucd1d \uac12 ${aggregatedData.todayData.totalValue.toFixed(2)}\n\ud83d\udcca **\uc804\uc77c \ub300\ube44**: \uac12 ${aggregatedData.comparison.valueChangePercent}% \ubcc0\ud654, \ubb38\uc11c \uc218 ${aggregatedData.comparison.countChangePercent}% \ubcc0\ud654\n\n---\n\n${geminiAnalysis}\n\n---\n\n\ud83d\udccb **\uc694\uc57d \ud1b5\uacc4**:\n- \ubd84\uc11d \uc2dc\uac04: ${new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })}\n- \ub370\uc774\ud130 \uc18c\uc2a4: Firestore \uceec\ub809\uc158 'reports'\n- \ubd84\uc11d \ub3c4\uad6c: Google Gemini AI`;\n\nreturn [{\n json: {\n finalReport,\n summary: {\n timestamp: new Date().toISOString(),\n dataCount: aggregatedData.todayData.totalCount,\n totalValue: aggregatedData.todayData.totalValue,\n changePercent: aggregatedData.comparison.valueChangePercent\n }\n }\n}];"
},
"id": "report-formatter",
"name": "\ucd5c\uc885 \ub9ac\ud3ec\ud2b8 \ud3ec\ub9f7\ud305",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
1560,
260
]
},
{
"parameters": {
"authentication": "accessToken",
"channel": "#daily-reports",
"text": "={{$json.finalReport}}",
"otherOptions": {
"thread_ts": "",
"reply_broadcast": false
}
},
"id": "slack-notification",
"name": "Slack \uc54c\ub9bc \uc804\uc1a1",
"type": "n8n-nodes-base.slack",
"typeVersion": 2,
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"position": [
1820,
260
]
},
{
"parameters": {
"authentication": "accessToken",
"channel": "#daily-reports",
"text": "\u274c **Firestore \uc815\uc624 \uc694\uc57d \uc2e4\ud328**\n\n\uc6cc\ud06c\ud50c\ub85c\uc6b0 \uc2e4\ud589 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n\n**\uc624\ub958 \uc815\ubcf4**:\n- \uc2dc\uac04: {{new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })}}\n- \ub178\ub4dc: {{$node.name}}\n- \uc624\ub958: {{$json.error.message || '\uc54c \uc218 \uc5c6\ub294 \uc624\ub958'}}\n\n\uad00\ub9ac\uc790\uc5d0\uac8c \ubb38\uc758\ud574\uc8fc\uc138\uc694.",
"otherOptions": {}
},
"id": "slack-error",
"name": "Slack \uc624\ub958 \uc54c\ub9bc",
"type": "n8n-nodes-base.slack",
"typeVersion": 2,
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"position": [
1300,
400
]
}
],
"connections": {
"Cron (\ub9e4\uc77c 12:00 KST)": {
"main": [
[
{
"node": "\uc2dc\uac04 \ubc94\uc704 \uacc4\uc0b0",
"type": "main",
"index": 0
}
]
]
},
"\uc2dc\uac04 \ubc94\uc704 \uacc4\uc0b0": {
"main": [
[
{
"node": "Firestore: \uc624\ub298 \ub370\uc774\ud130 \uc870\ud68c",
"type": "main",
"index": 0
},
{
"node": "Firestore: \uc5b4\uc81c \ub370\uc774\ud130 \uc870\ud68c",
"type": "main",
"index": 0
}
]
]
},
"Firestore: \uc624\ub298 \ub370\uc774\ud130 \uc870\ud68c": {
"main": [
[
{
"node": "\ub370\uc774\ud130 \uc9d1\uacc4 & \ubd84\uc11d \ud14d\uc2a4\ud2b8 \uc0dd\uc131",
"type": "main",
"index": 0
}
]
]
},
"Firestore: \uc5b4\uc81c \ub370\uc774\ud130 \uc870\ud68c": {
"main": [
[
{
"node": "\ub370\uc774\ud130 \uc9d1\uacc4 & \ubd84\uc11d \ud14d\uc2a4\ud2b8 \uc0dd\uc131",
"type": "main",
"index": 0
}
]
]
},
"\ub370\uc774\ud130 \uc9d1\uacc4 & \ubd84\uc11d \ud14d\uc2a4\ud2b8 \uc0dd\uc131": {
"main": [
[
{
"node": "Gemini AI \ubd84\uc11d",
"type": "main",
"index": 0
}
]
]
},
"Gemini AI \ubd84\uc11d": {
"main": [
[
{
"node": "\ucd5c\uc885 \ub9ac\ud3ec\ud2b8 \ud3ec\ub9f7\ud305",
"type": "main",
"index": 0
}
]
]
},
"\ucd5c\uc885 \ub9ac\ud3ec\ud2b8 \ud3ec\ub9f7\ud305": {
"main": [
[
{
"node": "Slack \uc54c\ub9bc \uc804\uc1a1",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [
{
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z",
"id": "firestore-summary",
"name": "Firestore Summary"
}
],
"triggerCount": 1,
"updatedAt": "2024-01-01T00:00:00.000Z",
"versionId": "1"
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googleFirebaseCloudFirestoreOAuth2ApigoogleGeminiApislackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Firestore 정오 요약 리포트 (Gemini). Uses googleFirebaseCloudFirestore, googleGemini, slack. Scheduled trigger; 9 nodes.
Source: https://github.com/LearningnRunning/TIL/blob/69d748ad3e15ae05c1da0e9fe0d92334067e4bb2/workflow/firestore-summary/n8n_workflow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
The AI Blog Creator with Gemini, Replicate Image, Supabase Publishing & Slack is a fully automated content generation and publishing workflow designed for modern marketing and SaaS teams. It automatic
This scheduled workflow pulls the latest AI-related articles from NewsAPI, uses Google Gemini to filter and draft a LinkedIn post plus image prompt, generates an image with OpenAI, uploads the image a
This workflow runs daily to compare procurement transactions in Google Sheets against an approved supplier list, flags unapproved (maverick) spend, uses Google Gemini to generate compliant alternative
This workflow runs daily to compare department budgets and actual spend from Google Sheets, calculates variance, uses Google Gemini to generate root-cause insights, sends Slack alerts when variance br
Automatically monitors restaurant ratings on Google Places daily, detects meaningful changes, uses Google Gemini AI to diagnose the root cause from real customer reviews, and delivers smart alerts to