70일차 자료 추가(답 제외), 69일차 flow.py call -v 반영

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mcchaeandClaude Opus 5.5 committed 2026-10-21 21:34:02 +09:00
1 parent eb1903bb51
commit 2478da876f
62 files changed
+4961 -30

No files matched your search

@@ -0,0 +1,283 @@
{
"name": "70 Notice kit (reference · Lab 01 example 2)",
"nodes": [
{
"name": "Kit webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
300
],
"parameters": {
"httpMethod": "POST",
"path": "my-notice-kit",
"responseMode": "responseNode",
"options": {}
}
},
{
"name": "Has kind and facts?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
200,
300
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c0",
"leftValue": "={{ $json.body.facts }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
},
{
"id": "c1",
"leftValue": "={{ $json.body.kind }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
420,
560
],
"parameters": {
"respondWith": "json",
"responseBody": "{ \"error\": \"kind and facts are required\" }",
"options": {
"responseCode": 400
}
}
},
{
"name": "Kind settings",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
420,
300
],
"parameters": {
"jsCode": "// The only per-kind part of the template: one row per notice kind.\nconst KINDS = {\n \"maintenance\": {\n \"label\": \"점검\",\n \"prefix\": \"[점검 안내]\",\n \"max_length\": 600,\n \"required_phrases\": [],\n \"extra\": \"Say clearly what cannot be used during the work and what to do after it.\"\n },\n \"security\": {\n \"label\": \"보안\",\n \"prefix\": \"[보안 안내]\",\n \"max_length\": 500,\n \"required_phrases\": [\n \"IT팀은 절대 비밀번호를 묻지 않습니다.\"\n ],\n \"extra\": \"Tell readers what to do and what never to do. Never ask for passwords.\"\n },\n \"training\": {\n \"label\": \"교육\",\n \"prefix\": \"[교육 안내]\",\n \"max_length\": 800,\n \"required_phrases\": [\n \"신청은 IT 포털에서 할 수 있습니다.\"\n ],\n \"extra\": \"Say who should attend, when and where, and how to apply.\"\n }\n};\nconst body = $('Kit webhook').first().json.body;\nconst kind = String(body.kind ?? '');\nreturn [{ json: { known: true, kind, ...KINDS[kind] } }];"
}
},
{
"name": "Writer agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
840,
300
],
"parameters": {
"promptType": "define",
"text": "=제목 머리말(prefix): {{ $('Kind settings').first().json.prefix }}\n꼭 넣을 문장: {{ ($('Kind settings').first().json.required_phrases ?? []).join(' / ') || '없음' }}\n이 종류의 주의: {{ $('Kind settings').first().json.extra }}\n요청: {{ $('Kit webhook').first().json.body.request || '공지' }}\n사실:\n{{ $('Kit webhook').first().json.body.facts }}",
"options": {
"systemMessage": "You write one company IT notice in Korean from the facts only. The first line is the title and it starts with the given prefix. Then 3 to 6 short lines. Copy every date and time exactly as written in the facts. Do not add promises, compensation, dates, people or contacts that are not in the facts. If required sentences are given, include each of them exactly as written. Return only the notice text.",
"maxIterations": 2
}
}
},
{
"name": "OpenAI chat model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"position": [
800,
520
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
}
},
{
"name": "Build draft",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1060,
300
],
"parameters": {
"jsCode": "// The writer's text, without code fences.\nreturn [{ json: { draft: String($json.output ?? '').trim().replace(/^```\\w*\\s*|```$/g, '').trim() } }];"
}
},
{
"name": "Rule checks",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1280,
300
],
"parameters": {
"jsCode": "// Rule checks. Same answer every time, so code, not the LLM.\nconst facts = String({ facts: $('Kit webhook').first().json.body.facts, draft: $json.draft, max_length: $('Kind settings').first().json.max_length, required_phrases: $('Kind settings').first().json.required_phrases }.facts ?? '');\nconst draft = String({ facts: $('Kit webhook').first().json.body.facts, draft: $json.draft, max_length: $('Kind settings').first().json.max_length, required_phrases: $('Kind settings').first().json.required_phrases }.draft ?? '');\nconst maxLength = Number({ facts: $('Kit webhook').first().json.body.facts, draft: $json.draft, max_length: $('Kind settings').first().json.max_length, required_phrases: $('Kind settings').first().json.required_phrases }.max_length ?? 600);\nconst required = Array.isArray({ facts: $('Kit webhook').first().json.body.facts, draft: $json.draft, max_length: $('Kind settings').first().json.max_length, required_phrases: $('Kind settings').first().json.required_phrases }.required_phrases) ? { facts: $('Kit webhook').first().json.body.facts, draft: $json.draft, max_length: $('Kind settings').first().json.max_length, required_phrases: $('Kind settings').first().json.required_phrases }.required_phrases : [];\nconst squeeze = text => text.replace(/\\s+/g, '');\nconst tokens = [...new Set(facts.match(/\\d{1,2}월\\s?\\d{1,2}일|\\d{1,2}:\\d{2}/g) ?? [])];\nconst issues = [];\nconst missing = tokens.filter(token => !squeeze(draft).includes(squeeze(token)));\nif (missing.length) issues.push({ code: 'missing_fact', detail: missing.join(', ') });\nconst personal = draft.match(/01[016789]-?\\d{3,4}-?\\d{4}|[\\w.+-]+@[\\w-]+\\.[\\w.]+/g) ?? [];\nif (personal.length) issues.push({ code: 'personal_info', detail: personal.join(', ') });\nif (draft.length > maxLength) issues.push({ code: 'too_long', detail: `${draft.length}자 (${maxLength}자 이하)` });\nconst absent = required.filter(phrase => !squeeze(draft).includes(squeeze(String(phrase))));\nif (absent.length) issues.push({ code: 'missing_phrase', detail: absent.join(' / ') });\n\nreturn [{ json: { passed: issues.length === 0, codes: issues.map(issue => issue.code), issues } }];"
}
},
{
"name": "Build result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1500,
300
],
"parameters": {
"jsCode": "// The kit's contract: { kind, route, published, notice, codes }.\nconst review = $json;\nconst draft = $('Build draft').first().json.draft;\nreturn [{ json: { kind: $('Kind settings').first().json.kind, route: review.passed ? '게시 가능' : '담당자 확인',\n published: review.passed === true, notice: draft, codes: review.codes ?? [] } }];"
}
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1720,
300
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
}
}
],
"connections": {
"Kit webhook": {
"main": [
[
{
"node": "Has kind and facts?",
"type": "main",
"index": 0
}
]
]
},
"Has kind and facts?": {
"main": [
[
{
"node": "Kind settings",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Kind settings": {
"main": [
[
{
"node": "Writer agent",
"type": "main",
"index": 0
}
]
]
},
"OpenAI chat model": {
"ai_languageModel": [
[
{
"node": "Writer agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Writer agent": {
"main": [
[
{
"node": "Build draft",
"type": "main",
"index": 0
}
]
]
},
"Build draft": {
"main": [
[
{
"node": "Rule checks",
"type": "main",
"index": 0
}
]
]
},
"Rule checks": {
"main": [
[
{
"node": "Build result",
"type": "main",
"index": 0
}
]
]
},
"Build result": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,132 @@
{
"name": "70 Test · Rule check module",
"nodes": [
{
"name": "Test webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
300
],
"parameters": {
"httpMethod": "POST",
"path": "my-rule-check",
"responseMode": "responseNode",
"options": {}
}
},
{
"name": "Rule check module",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
240,
300
],
"parameters": {
"workflowId": {
"__rl": true,
"mode": "id",
"value": "modRuleCheck01"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"facts": "={{ $json.body.facts }}",
"draft": "={{ $json.body.draft }}",
"max_length": "={{ $json.body.max_length ?? 600 }}",
"required_phrases": "={{ $json.body.required_phrases ?? [] }}"
},
"matchingColumns": [],
"schema": [
{
"id": "facts",
"displayName": "facts",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "draft",
"displayName": "draft",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "max_length",
"displayName": "max_length",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "number",
"removed": false
},
{
"id": "required_phrases",
"displayName": "required_phrases",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "array",
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
}
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
480,
300
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
}
}
],
"connections": {
"Test webhook": {
"main": [
[
{
"node": "Rule check module",
"type": "main",
"index": 0
}
]
]
},
"Rule check module": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,66 @@
{
"id": "modRuleCheck01",
"name": "70 Module · Rule check (reference · Lab 02 example 1)",
"nodes": [
{
"name": "When called by another workflow",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
0,
300
],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": {
"values": [
{
"name": "facts",
"type": "string"
},
{
"name": "draft",
"type": "string"
},
{
"name": "max_length",
"type": "number"
},
{
"name": "required_phrases",
"type": "array"
}
]
}
}
},
{
"name": "Rule checks",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
260,
300
],
"parameters": {
"jsCode": "// Rule checks. Same answer every time, so code, not the LLM.\nconst facts = String($json.facts ?? '');\nconst draft = String($json.draft ?? '');\nconst maxLength = Number($json.max_length ?? 600);\nconst required = Array.isArray($json.required_phrases) ? $json.required_phrases : [];\nconst squeeze = text => text.replace(/\\s+/g, '');\nconst tokens = [...new Set(facts.match(/\\d{1,2}월\\s?\\d{1,2}일|\\d{1,2}:\\d{2}/g) ?? [])];\nconst issues = [];\nconst missing = tokens.filter(token => !squeeze(draft).includes(squeeze(token)));\nif (missing.length) issues.push({ code: 'missing_fact', detail: missing.join(', ') });\nconst personal = draft.match(/01[016789]-?\\d{3,4}-?\\d{4}|[\\w.+-]+@[\\w-]+\\.[\\w.]+/g) ?? [];\nif (personal.length) issues.push({ code: 'personal_info', detail: personal.join(', ') });\nif (draft.length > maxLength) issues.push({ code: 'too_long', detail: `${draft.length}자 (${maxLength}자 이하)` });\nconst absent = required.filter(phrase => !squeeze(draft).includes(squeeze(String(phrase))));\nif (absent.length) issues.push({ code: 'missing_phrase', detail: absent.join(' / ') });\n\nreturn [{ json: { passed: issues.length === 0, codes: issues.map(issue => issue.code), issues } }];"
}
}
],
"connections": {
"When called by another workflow": {
"main": [
[
{
"node": "Rule checks",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,342 @@
{
"name": "70 Notice kit with module (reference · Lab 02 example 2)",
"nodes": [
{
"name": "Kit webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
300
],
"parameters": {
"httpMethod": "POST",
"path": "my-notice-kit",
"responseMode": "responseNode",
"options": {}
}
},
{
"name": "Has kind and facts?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
200,
300
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c0",
"leftValue": "={{ $json.body.facts }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
},
{
"id": "c1",
"leftValue": "={{ $json.body.kind }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
420,
560
],
"parameters": {
"respondWith": "json",
"responseBody": "{ \"error\": \"kind and facts are required\" }",
"options": {
"responseCode": 400
}
}
},
{
"name": "Kind settings",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
420,
300
],
"parameters": {
"jsCode": "// The only per-kind part of the template: one row per notice kind.\nconst KINDS = {\n \"maintenance\": {\n \"label\": \"점검\",\n \"prefix\": \"[점검 안내]\",\n \"max_length\": 600,\n \"required_phrases\": [],\n \"extra\": \"Say clearly what cannot be used during the work and what to do after it.\"\n },\n \"security\": {\n \"label\": \"보안\",\n \"prefix\": \"[보안 안내]\",\n \"max_length\": 500,\n \"required_phrases\": [\n \"IT팀은 절대 비밀번호를 묻지 않습니다.\"\n ],\n \"extra\": \"Tell readers what to do and what never to do. Never ask for passwords.\"\n },\n \"training\": {\n \"label\": \"교육\",\n \"prefix\": \"[교육 안내]\",\n \"max_length\": 800,\n \"required_phrases\": [\n \"신청은 IT 포털에서 할 수 있습니다.\"\n ],\n \"extra\": \"Say who should attend, when and where, and how to apply.\"\n }\n};\nconst body = $('Kit webhook').first().json.body;\nconst kind = String(body.kind ?? '');\nreturn [{ json: { known: true, kind, ...KINDS[kind] } }];"
}
},
{
"name": "Writer agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
840,
300
],
"parameters": {
"promptType": "define",
"text": "=제목 머리말(prefix): {{ $('Kind settings').first().json.prefix }}\n꼭 넣을 문장: {{ ($('Kind settings').first().json.required_phrases ?? []).join(' / ') || '없음' }}\n이 종류의 주의: {{ $('Kind settings').first().json.extra }}\n요청: {{ $('Kit webhook').first().json.body.request || '공지' }}\n사실:\n{{ $('Kit webhook').first().json.body.facts }}",
"options": {
"systemMessage": "You write one company IT notice in Korean from the facts only. The first line is the title and it starts with the given prefix. Then 3 to 6 short lines. Copy every date and time exactly as written in the facts. Do not add promises, compensation, dates, people or contacts that are not in the facts. If required sentences are given, include each of them exactly as written. Return only the notice text.",
"maxIterations": 2
}
}
},
{
"name": "OpenAI chat model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"position": [
800,
520
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
}
},
{
"name": "Build draft",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1060,
300
],
"parameters": {
"jsCode": "// The writer's text, without code fences.\nreturn [{ json: { draft: String($json.output ?? '').trim().replace(/^```\\w*\\s*|```$/g, '').trim() } }];"
}
},
{
"name": "Rule check module",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1280,
300
],
"parameters": {
"workflowId": {
"__rl": true,
"mode": "id",
"value": "modRuleCheck01"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"facts": "={{ $('Kit webhook').first().json.body.facts }}",
"draft": "={{ $json.draft }}",
"max_length": "={{ $('Kind settings').first().json.max_length }}",
"required_phrases": "={{ $('Kind settings').first().json.required_phrases }}"
},
"matchingColumns": [],
"schema": [
{
"id": "facts",
"displayName": "facts",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "draft",
"displayName": "draft",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "max_length",
"displayName": "max_length",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "number",
"removed": false
},
{
"id": "required_phrases",
"displayName": "required_phrases",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "array",
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
}
},
{
"name": "Build result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1500,
300
],
"parameters": {
"jsCode": "// The kit's contract: { kind, route, published, notice, codes }.\nconst review = $json;\nconst draft = $('Build draft').first().json.draft;\nreturn [{ json: { kind: $('Kind settings').first().json.kind, route: review.passed ? '게시 가능' : '담당자 확인',\n published: review.passed === true, notice: draft, codes: review.codes ?? [] } }];"
}
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1720,
300
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
}
}
],
"connections": {
"Kit webhook": {
"main": [
[
{
"node": "Has kind and facts?",
"type": "main",
"index": 0
}
]
]
},
"Has kind and facts?": {
"main": [
[
{
"node": "Kind settings",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Kind settings": {
"main": [
[
{
"node": "Writer agent",
"type": "main",
"index": 0
}
]
]
},
"OpenAI chat model": {
"ai_languageModel": [
[
{
"node": "Writer agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Writer agent": {
"main": [
[
{
"node": "Build draft",
"type": "main",
"index": 0
}
]
]
},
"Build draft": {
"main": [
[
{
"node": "Rule check module",
"type": "main",
"index": 0
}
]
]
},
"Rule check module": {
"main": [
[
{
"node": "Build result",
"type": "main",
"index": 0
}
]
]
},
"Build result": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,143 @@
{
"name": "70 Test · Rule check module v1.1",
"nodes": [
{
"name": "Test webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
300
],
"parameters": {
"httpMethod": "POST",
"path": "my-rule-check",
"responseMode": "responseNode",
"options": {}
}
},
{
"name": "Rule check module",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
240,
300
],
"parameters": {
"workflowId": {
"__rl": true,
"mode": "id",
"value": "modRuleCheck01"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"facts": "={{ $json.body.facts }}",
"draft": "={{ $json.body.draft }}",
"max_length": "={{ $json.body.max_length ?? 600 }}",
"required_phrases": "={{ $json.body.required_phrases ?? [] }}",
"allowed_domains": "={{ $json.body.allowed_domains ?? [] }}"
},
"matchingColumns": [],
"schema": [
{
"id": "facts",
"displayName": "facts",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "draft",
"displayName": "draft",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "max_length",
"displayName": "max_length",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "number",
"removed": false
},
{
"id": "required_phrases",
"displayName": "required_phrases",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "array",
"removed": false
},
{
"id": "allowed_domains",
"displayName": "allowed_domains",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "array",
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
}
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
480,
300
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
}
}
],
"connections": {
"Test webhook": {
"main": [
[
{
"node": "Rule check module",
"type": "main",
"index": 0
}
]
]
},
"Rule check module": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,70 @@
{
"id": "modRuleCheck01",
"name": "70 Module · Rule check v1.1 (reference · Lab 03 example 1)",
"nodes": [
{
"name": "When called by another workflow",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
0,
300
],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": {
"values": [
{
"name": "facts",
"type": "string"
},
{
"name": "draft",
"type": "string"
},
{
"name": "max_length",
"type": "number"
},
{
"name": "required_phrases",
"type": "array"
},
{
"name": "allowed_domains",
"type": "array"
}
]
}
}
},
{
"name": "Rule checks",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
260,
300
],
"parameters": {
"jsCode": "// Rule checks. Same answer every time, so code, not the LLM.\nconst facts = String($json.facts ?? '');\nconst draft = String($json.draft ?? '');\nconst maxLength = Number($json.max_length ?? 600);\nconst required = Array.isArray($json.required_phrases) ? $json.required_phrases : [];\nconst squeeze = text => text.replace(/\\s+/g, '');\nconst tokens = [...new Set(facts.match(/\\d{1,2}월\\s?\\d{1,2}일|\\d{1,2}:\\d{2}/g) ?? [])];\nconst issues = [];\nconst missing = tokens.filter(token => !squeeze(draft).includes(squeeze(token)));\nif (missing.length) issues.push({ code: 'missing_fact', detail: missing.join(', ') });\nconst personal = draft.match(/01[016789]-?\\d{3,4}-?\\d{4}|[\\w.+-]+@[\\w-]+\\.[\\w.]+/g) ?? [];\nif (personal.length) issues.push({ code: 'personal_info', detail: personal.join(', ') });\nif (draft.length > maxLength) issues.push({ code: 'too_long', detail: `${draft.length}자 (${maxLength}자 이하)` });\nconst absent = required.filter(phrase => !squeeze(draft).includes(squeeze(String(phrase))));\nif (absent.length) issues.push({ code: 'missing_phrase', detail: absent.join(' / ') });\nconst allowed = Array.isArray($json.allowed_domains) && $json.allowed_domains.length ? $json.allowed_domains : ['intra.example.com'];\nconst links = draft.match(/https?:\\/\\/[^\\s)]+/g) ?? [];\n// Host name by text (the Code sandbox has no URL class).\nconst host = url => url.replace(/^https?:\\/\\//, '').split(/[\\/?#:]/)[0].toLowerCase();\nconst outside = links.filter(url => !allowed.includes(host(url)));\nif (outside.length) issues.push({ code: 'external_link', detail: outside.join(', ') });\nreturn [{ json: { passed: issues.length === 0, codes: issues.map(issue => issue.code), issues } }];"
}
}
],
"connections": {
"When called by another workflow": {
"main": [
[
{
"node": "Rule checks",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,342 @@
{
"name": "70 Notice kit on module v2 (reference · Lab 03 example 2)",
"nodes": [
{
"name": "Kit webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
300
],
"parameters": {
"httpMethod": "POST",
"path": "my-notice-kit",
"responseMode": "responseNode",
"options": {}
}
},
{
"name": "Has kind and facts?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
200,
300
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c0",
"leftValue": "={{ $json.body.facts }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
},
{
"id": "c1",
"leftValue": "={{ $json.body.kind }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"name": "Reject empty",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
420,
560
],
"parameters": {
"respondWith": "json",
"responseBody": "{ \"error\": \"kind and facts are required\" }",
"options": {
"responseCode": 400
}
}
},
{
"name": "Kind settings",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
420,
300
],
"parameters": {
"jsCode": "// The only per-kind part of the template: one row per notice kind.\nconst KINDS = {\n \"maintenance\": {\n \"label\": \"점검\",\n \"prefix\": \"[점검 안내]\",\n \"max_length\": 600,\n \"required_phrases\": [],\n \"extra\": \"Say clearly what cannot be used during the work and what to do after it.\"\n },\n \"security\": {\n \"label\": \"보안\",\n \"prefix\": \"[보안 안내]\",\n \"max_length\": 500,\n \"required_phrases\": [\n \"IT팀은 절대 비밀번호를 묻지 않습니다.\"\n ],\n \"extra\": \"Tell readers what to do and what never to do. Never ask for passwords.\"\n },\n \"training\": {\n \"label\": \"교육\",\n \"prefix\": \"[교육 안내]\",\n \"max_length\": 800,\n \"required_phrases\": [\n \"신청은 IT 포털에서 할 수 있습니다.\"\n ],\n \"extra\": \"Say who should attend, when and where, and how to apply.\"\n }\n};\nconst body = $('Kit webhook').first().json.body;\nconst kind = String(body.kind ?? '');\nreturn [{ json: { known: true, kind, ...KINDS[kind] } }];"
}
},
{
"name": "Writer agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
840,
300
],
"parameters": {
"promptType": "define",
"text": "=제목 머리말(prefix): {{ $('Kind settings').first().json.prefix }}\n꼭 넣을 문장: {{ ($('Kind settings').first().json.required_phrases ?? []).join(' / ') || '없음' }}\n이 종류의 주의: {{ $('Kind settings').first().json.extra }}\n요청: {{ $('Kit webhook').first().json.body.request || '공지' }}\n사실:\n{{ $('Kit webhook').first().json.body.facts }}",
"options": {
"systemMessage": "You write one company IT notice in Korean from the facts only. The first line is the title and it starts with the given prefix. Then 3 to 6 short lines. Copy every date and time exactly as written in the facts. Do not add promises, compensation, dates, people or contacts that are not in the facts. If required sentences are given, include each of them exactly as written. Return only the notice text.",
"maxIterations": 2
}
}
},
{
"name": "OpenAI chat model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"position": [
800,
520
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0
}
},
"credentials": {
"openAiApi": {
"id": "openAiCourse0001",
"name": "OpenAI (.env)"
}
}
},
{
"name": "Build draft",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1060,
300
],
"parameters": {
"jsCode": "// The writer's text, without code fences.\nreturn [{ json: { draft: String($json.output ?? '').trim().replace(/^```\\w*\\s*|```$/g, '').trim() } }];"
}
},
{
"name": "Rule check module",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1280,
300
],
"parameters": {
"workflowId": {
"__rl": true,
"mode": "id",
"value": "modRuleCheck02"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"facts": "={{ $('Kit webhook').first().json.body.facts }}",
"draft": "={{ $json.draft }}",
"max_length": "={{ $('Kind settings').first().json.max_length }}",
"required_phrases": "={{ $('Kind settings').first().json.required_phrases }}"
},
"matchingColumns": [],
"schema": [
{
"id": "facts",
"displayName": "facts",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "draft",
"displayName": "draft",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "max_length",
"displayName": "max_length",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "number",
"removed": false
},
{
"id": "required_phrases",
"displayName": "required_phrases",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "array",
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
}
},
{
"name": "Build result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1500,
300
],
"parameters": {
"jsCode": "// The kit's contract with module v2: blocking stops, warnings are reported.\nconst review = $json;\nconst draft = $('Build draft').first().json.draft;\nconst ok = review.passed === true;\nreturn [{ json: { kind: $('Kind settings').first().json.kind, route: ok ? '게시 가능' : '담당자 확인',\n published: ok, notice: draft, blocking: review.blocking ?? [], warnings: review.warnings ?? [],\n // Keep the old field so callers of the kit do not break.\n codes: [...(review.blocking ?? []), ...(review.warnings ?? [])] } }];"
}
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1720,
300
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
}
}
],
"connections": {
"Kit webhook": {
"main": [
[
{
"node": "Has kind and facts?",
"type": "main",
"index": 0
}
]
]
},
"Has kind and facts?": {
"main": [
[
{
"node": "Kind settings",
"type": "main",
"index": 0
}
],
[
{
"node": "Reject empty",
"type": "main",
"index": 0
}
]
]
},
"Kind settings": {
"main": [
[
{
"node": "Writer agent",
"type": "main",
"index": 0
}
]
]
},
"OpenAI chat model": {
"ai_languageModel": [
[
{
"node": "Writer agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Writer agent": {
"main": [
[
{
"node": "Build draft",
"type": "main",
"index": 0
}
]
]
},
"Build draft": {
"main": [
[
{
"node": "Rule check module",
"type": "main",
"index": 0
}
]
]
},
"Rule check module": {
"main": [
[
{
"node": "Build result",
"type": "main",
"index": 0
}
]
]
},
"Build result": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,143 @@
{
"name": "70 Test · Rule check module v2",
"nodes": [
{
"name": "Test webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
300
],
"parameters": {
"httpMethod": "POST",
"path": "my-rule-check-v2",
"responseMode": "responseNode",
"options": {}
}
},
{
"name": "Rule check module",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
240,
300
],
"parameters": {
"workflowId": {
"__rl": true,
"mode": "id",
"value": "modRuleCheck02"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"facts": "={{ $json.body.facts }}",
"draft": "={{ $json.body.draft }}",
"max_length": "={{ $json.body.max_length ?? 600 }}",
"required_phrases": "={{ $json.body.required_phrases ?? [] }}",
"allowed_domains": "={{ $json.body.allowed_domains ?? [] }}"
},
"matchingColumns": [],
"schema": [
{
"id": "facts",
"displayName": "facts",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "draft",
"displayName": "draft",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "max_length",
"displayName": "max_length",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "number",
"removed": false
},
{
"id": "required_phrases",
"displayName": "required_phrases",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "array",
"removed": false
},
{
"id": "allowed_domains",
"displayName": "allowed_domains",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "array",
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
}
},
{
"name": "Send result",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
480,
300
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
}
}
],
"connections": {
"Test webhook": {
"main": [
[
{
"node": "Rule check module",
"type": "main",
"index": 0
}
]
]
},
"Rule check module": {
"main": [
[
{
"node": "Send result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,70 @@
{
"id": "modRuleCheck02",
"name": "70 Module · Rule check v2 (reference · Lab 03 example 2)",
"nodes": [
{
"name": "When called by another workflow",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
0,
300
],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": {
"values": [
{
"name": "facts",
"type": "string"
},
{
"name": "draft",
"type": "string"
},
{
"name": "max_length",
"type": "number"
},
{
"name": "required_phrases",
"type": "array"
},
{
"name": "allowed_domains",
"type": "array"
}
]
}
}
},
{
"name": "Rule checks",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
260,
300
],
"parameters": {
"jsCode": "// Rule checks. Same answer every time, so code, not the LLM.\nconst facts = String($json.facts ?? '');\nconst draft = String($json.draft ?? '');\nconst maxLength = Number($json.max_length ?? 600);\nconst required = Array.isArray($json.required_phrases) ? $json.required_phrases : [];\nconst squeeze = text => text.replace(/\\s+/g, '');\nconst tokens = [...new Set(facts.match(/\\d{1,2}월\\s?\\d{1,2}일|\\d{1,2}:\\d{2}/g) ?? [])];\nconst issues = [];\nconst missing = tokens.filter(token => !squeeze(draft).includes(squeeze(token)));\nif (missing.length) issues.push({ code: 'missing_fact', detail: missing.join(', ') });\nconst personal = draft.match(/01[016789]-?\\d{3,4}-?\\d{4}|[\\w.+-]+@[\\w-]+\\.[\\w.]+/g) ?? [];\nif (personal.length) issues.push({ code: 'personal_info', detail: personal.join(', ') });\nif (draft.length > maxLength) issues.push({ code: 'too_long', detail: `${draft.length}자 (${maxLength}자 이하)` });\nconst absent = required.filter(phrase => !squeeze(draft).includes(squeeze(String(phrase))));\nif (absent.length) issues.push({ code: 'missing_phrase', detail: absent.join(' / ') });\nconst allowed = Array.isArray($json.allowed_domains) && $json.allowed_domains.length ? $json.allowed_domains : ['intra.example.com'];\nconst links = draft.match(/https?:\\/\\/[^\\s)]+/g) ?? [];\n// Host name by text (the Code sandbox has no URL class).\nconst host = url => url.replace(/^https?:\\/\\//, '').split(/[\\/?#:]/)[0].toLowerCase();\nconst outside = links.filter(url => !allowed.includes(host(url)));\nif (outside.length) issues.push({ code: 'external_link', detail: outside.join(', ') });\nreturn [{ json: (() => {\n const WARNING = ['too_long'];\n const blocking = issues.filter(issue => !WARNING.includes(issue.code)).map(issue => issue.code);\n const warnings = issues.filter(issue => WARNING.includes(issue.code)).map(issue => issue.code);\n return { passed: blocking.length === 0, blocking, warnings, issues, version: 2 };\n})() }];"
}
}
],
"connections": {
"When called by another workflow": {
"main": [
[
{
"node": "Rule checks",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}