n8n_3_CAD-BIM-批量변환器-管道
고급
이것은Document Extraction, Multimodal AI분야의자동화 워크플로우로, 82개의 노드를 포함합니다.주로 If, Set, Code, Merge, ManualTrigger 등의 노드를 사용하며. 批量 CAD/BIM 파일을 XLSX/DAE로 변환, 검증과 보고서 포함
사전 요구사항
- •OpenAI API Key
- •Anthropic API Key
- •Google Gemini API Key
사용된 노드 (82)
워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
"id": "u5MdPlYjhriENe3R",
"meta": {
"instanceId": "faa70e11b7175129a74fd834d3451fdc1862589b16d68ded03f91ca7b1ecca12"
},
"name": "n8n_3_CAD-BIM-Batch-Converter-Pipeline",
"tags": [],
"nodes": [
{
"id": "be5c0de2-0df1-4c35-8b0e-26f8d1911bda",
"name": "고정 메모23",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1088,
224
],
"parameters": {
"width": 376,
"height": 368,
"content": "## 📋 Quick Start Guide\n\n**1️⃣ Configure Settings**\nEdit \"Set Configuration Parameters\":\n- `converter_path`: Path to RvtExporter.exe\n- `source_folder`: Your CAD files location\n- `output_folder`: Where to save results\n- `file_extension`: .rvt, .ifc, .dwg, or .dgn\n\n**2️⃣ Run Pipeline**\nClick \"Execute Workflow\"\n\n**3️⃣ Get Results**\n✅ Excel data files\n✅ 3D DAE models\n✅ HTML report"
},
"typeVersion": 1
},
{
"id": "d8a6fce0-af86-4769-8911-afb6c665f38d",
"name": "고정 메모25",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1088,
608
],
"parameters": {
"color": 2,
"width": 380,
"height": 224,
"content": "## 🎓 Video Tutorials\n\n**📹 [CAD-BIM Pipeline Tutorial](https://www.youtube.com/watch?v=PMTZNRFjD6c)**\nComplete walkthrough of CAD-BIM automation\n\n**⚡ [Automated Validation](https://www.youtube.com/watch?v=p84AmP2dcvg)**\nStop manual BIM checking forever\n\n💡 **Pro tip:** Watch before first run!"
},
"typeVersion": 1
},
{
"id": "1182ee14-cf89-45ee-96dc-d96a6864dcfa",
"name": "고정 메모28",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1088,
1168
],
"parameters": {
"color": 2,
"width": 372,
"height": 244,
"content": "## 🆘 Support\n\n**Resources:**\n🌐 [DataDrivenConstruction.io](https://datadrivenconstruction.io)\n📧 support@datadrivenconstruction.io\n💬 [Community Forum](https://t.me/datadrivenconstruction)\n\n**Documentation:**\n📚 [n8n Docs](https://docs.n8n.io)\n🎥 [YouTube Channel](https://youtube.com/@datadrivenconstruction)"
},
"typeVersion": 1
},
{
"id": "8bd00e43-cb87-489a-87b6-a6258262a5bf",
"name": "고정 메모29",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1088,
848
],
"parameters": {
"color": 2,
"width": 380,
"height": 304,
"content": "## 🎯 Use Cases\n\n**Weekly Reports**\nAutomate Monday conversions\n\n**Quality Control**\nValidate before client delivery\n\n**Archive Projects**\nConvert old files to lightweight formats\n\n**BI Integration**\nFeed Excel data to Power BI"
},
"typeVersion": 1
},
{
"id": "3ccefd7c-ade0-4125-952c-928115a49ebf",
"name": "고정 메모31",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1088,
112
],
"parameters": {
"width": 372,
"height": 100,
"content": "⭐ **If you find our tools helpful**, please consider starring our repository on [GitHub](https://github.com/datadrivenconstruction/cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto). **Your support helps us** improve and continue developing open solutions for the community!\n"
},
"typeVersion": 1
},
{
"id": "c5388249-6dc3-4070-9057-2b5668251832",
"name": "고정 메모",
"type": "n8n-nodes-base.stickyNote",
"position": [
-688,
112
],
"parameters": {
"width": 520,
"height": 1940,
"content": "## 📋 Options Parameter Configuration Guide\n\n## 🎯 Overview\nThe Options parameter allows you to customize the export process with various flags and settings. Multiple options can be combined using spaces.\n\n## 🚀 Available Options\n\n### 📦 Export Modes\n| Mode | Description |\n|------|-------------|\n| `basic` | Minimal export with essential data only |\n| `standard` | Default export level with standard properties |\n| `complete` | Full export including all available data |\n| `custom` | Custom export using category file specification |\n\n### 🛠️ Feature Flags\n| Option | Description |\n|--------|-------------|\n| `bbox` | Add BoundingBox geometry of each element in XLSX |\n| `schedule` | Export all Schedules |\n| `sheets2pdf` | Export all Sheets to PDF format |\n| `[<output file>]` | Specify custom output file path |\n| `[<category file>]` | Text file with category names (required for custom mode) |\n\n### 🚫 Disable Options\n| Option | Description |\n|--------|-------------|\n| `-no-xlsx` | Disable export to .xlsx format |\n| `-no-collada` | Disable export to .dae format |\n\n---\n\n## 💡 Usage Examples\n\n### Example 1: **Basic Export with BoundingBox**\n```bash\nOptions: bbox basic\n```\n> Exports basic data with BoundingBox geometry included\n\n### Example 2: **Complete Export with Schedules**\n```bash\nOptions: complete schedule sheets2pdf\n```\n> Full data export including all schedules and sheets converted to PDF\n\n### Example 3: **Custom Mode with Category File**\n```bash\nOptions: custom categories.txt bbox\n```\n> Custom export using categories.txt file with BoundingBox geometry\n\n### Example 4: **Export Without Specific Formats**\n```bash\nOptions: standard -no-xlsx -no-collada\n```\n> Standard export excluding XLSX and Collada formats\n\n### Example 5: **Schedules Only Export**\n```bash\nOptions: schedule -no-collada\n```\n> Export only schedules without Collada format\n\n### Example 6: **Custom Output Path**\n```bash\nOptions: C:\\Output\\result.xlsx complete bbox\n```\n> Complete export with BoundingBox to specific output file\n\n---\n\n## ⚡ Quick Tips\n- **Combine multiple options** by separating them with spaces\n- **Order matters** - export mode should come before other options\n- **Custom mode** requires a category file to function properly\n- **Disable flags** can be used to exclude unwanted formats\n\n---\n\n## 📝 Notes\n- All options are case-sensitive\n- Invalid combinations will be ignored\n- Default behavior applies when no options are specified"
},
"typeVersion": 1
},
{
"id": "2fe7360d-df50-4eb7-8799-441e7aed7a1a",
"name": "수동 트리거",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-80,
704
],
"parameters": {},
"typeVersion": 1
},
{
"id": "614d8fe1-6c80-4cb0-b941-5aa3998a2321",
"name": "파이프라인 시작 시간 캡처1",
"type": "n8n-nodes-base.code",
"position": [
144,
656
],
"parameters": {
"jsCode": "// Capture pipeline start time at the very beginning\nconst now = new Date();\nreturn [{\n json: {\n pipeline_start_time: now.toISOString(),\n pipeline_start_timestamp: now.getTime(),\n pipeline_start_message: `Pipeline started at ${now.toLocaleString()}`\n }\n}];"
},
"typeVersion": 2
},
{
"id": "e64288d1-9d53-4036-afde-437c9892a927",
"name": "파이프라인 시작과 구성 병합1",
"type": "n8n-nodes-base.merge",
"position": [
688,
672
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"typeVersion": 3
},
{
"id": "7b214d29-a619-4a41-8cfa-67f074470b89",
"name": "구성 매개변수 설정1",
"type": "n8n-nodes-base.set",
"position": [
416,
512
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "converter-path",
"name": "converter_path",
"type": "string",
"value": "C:\\Users\\Artem Boiko\\Desktop\\n8n\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\DDC_Converter_Revit\\datadrivenlibs\\RvtExporter.exe"
},
{
"id": "source-folder",
"name": "source_folder",
"type": "string",
"value": "C:\\Users\\Artem Boiko\\Desktop\\n8n\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\Sample_Projects"
},
{
"id": "output-folder",
"name": "output_folder",
"type": "string",
"value": "C:\\Users\\Artem Boiko\\Desktop\\n8n\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\Sample_Projects"
},
{
"id": "include-subfolders",
"name": "include_subfolders",
"type": "boolean",
"value": false
},
{
"id": "file-extension",
"name": "file_extension",
"type": "string",
"value": ".rvt"
},
{
"id": "a811f4ba-b7a5-4774-a1fa-90d9c43a0de6",
"name": "options",
"type": "string",
"value": "basic schedule -no-collada"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "53648934-0bb7-49d3-83f9-ab4280bb3ec9",
"name": "CAD 파일 찾기1",
"type": "n8n-nodes-base.executeCommand",
"position": [
848,
688
],
"parameters": {
"command": "=powershell -Command \"Get-ChildItem -LiteralPath '{{ $json.source_folder }}' -Filter '*{{ $json.file_extension }}' {{ $json.include_subfolders ? '-Recurse' : '' }} | Select-Object -ExpandProperty FullName\""
},
"typeVersion": 1
},
{
"id": "894a4fae-cd2f-4418-b35b-fc6a06cda820",
"name": "구성과 검색 결과 병합1",
"type": "n8n-nodes-base.merge",
"position": [
1008,
576
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"typeVersion": 3
},
{
"id": "cc6f17d1-4393-4c77-947a-53c42c01543c",
"name": "파일 목록 처리1",
"type": "n8n-nodes-base.code",
"position": [
1168,
576
],
"parameters": {
"jsCode": "// Process file list with configuration and pipeline start time\nconst configData = $input.first().json || {};\nconst searchData = $input.last().json || {};\n\nconst output = searchData.stdout || '';\nconst stderr = searchData.stderr || '';\n\n// Preserve pipeline start time\nconst pipeline_start_time = configData.pipeline_start_time || '';\nconst pipeline_start_timestamp = configData.pipeline_start_timestamp || Date.now();\n\nconsole.log('=== Processing File List ===');\nconsole.log('Pipeline start time:', pipeline_start_time);\nconsole.log('Config data:', configData);\nconsole.log('Raw output:', output);\nconsole.log('Files found:', output ? output.split(/\\r?\\n/).filter(x => x.trim()).length : 0);\n\nconst config = {\n converter_path: configData.converter_path || '',\n source_folder: configData.source_folder || '',\n output_folder: configData.output_folder || '',\n include_subfolders: configData.include_subfolders || false,\n file_extension: configData.file_extension || '.rvt',\n options: configData.options || ''\n};\n\nif (!output || !output.trim()) {\n return [{\n json: {\n files: [],\n total_files: 0,\n message: `No files found with extension '${config.file_extension}' in ${config.source_folder}`,\n error: stderr || 'No output from search command',\n config: config,\n pipeline_start_time: pipeline_start_time,\n pipeline_start_timestamp: pipeline_start_timestamp\n }\n }];\n}\n\nlet normalizedExtension = (config.file_extension || '').trim();\nif (normalizedExtension && !normalizedExtension.startsWith('.')) {\n normalizedExtension = '.' + normalizedExtension;\n}\n\nlet rawPaths = [];\ntry {\n rawPaths = output.trim().split(/\\r?\\n/)\n .map(path => (path || '').trim())\n .filter(path => path && path.length > 0);\n} catch (error) {\n console.log('Error splitting output:', error);\n return [{\n json: {\n files: [],\n total_files: 0,\n message: `Error processing search results: ${error.message}`,\n config: config,\n pipeline_start_time: pipeline_start_time,\n pipeline_start_timestamp: pipeline_start_timestamp\n }\n }];\n}\n\nconst filePaths = rawPaths.filter(path => {\n if (!path || !normalizedExtension) return false;\n const lowerPath = path.toLowerCase();\n const lowerExt = normalizedExtension.toLowerCase();\n return lowerPath.endsWith(lowerExt);\n});\n\nif (filePaths.length === 0) {\n return [{\n json: {\n files: [],\n total_files: 0,\n message: `No files found with extension '${normalizedExtension}' in ${config.source_folder}`,\n config: config,\n pipeline_start_time: pipeline_start_time,\n pipeline_start_timestamp: pipeline_start_timestamp\n }\n }];\n}\n\nconst files = filePaths.map((filePath, index) => {\n let fileName = 'unknown_file';\n let fileNameWithoutExt = 'unknown_file';\n \n try {\n if (filePath && typeof filePath === 'string') {\n const normalizedPath = filePath.replace(/\\\\/g, '/');\n const pathParts = normalizedPath.split('/');\n fileName = pathParts[pathParts.length - 1] || 'unknown_file';\n \n console.log(`Processing file ${index + 1}: ${fileName} from path: ${filePath}`);\n \n const lastDotIndex = fileName.lastIndexOf('.');\n if (lastDotIndex > 0) {\n fileNameWithoutExt = fileName.substring(0, lastDotIndex);\n } else {\n fileNameWithoutExt = fileName;\n }\n }\n } catch (error) {\n console.log(`Error parsing file path ${filePath}:`, error);\n }\n \n const extWithoutDot = normalizedExtension ? normalizedExtension.substring(1) : 'unknown';\n \n return {\n index: index + 1,\n file_path: filePath,\n file_name: fileName,\n file_name_without_ext: fileNameWithoutExt,\n converter_path: config.converter_path,\n expected_output: `${config.output_folder}\\\\${fileNameWithoutExt}_${extWithoutDot}.xlsx`,\n expected_output_dae: `${config.output_folder}\\\\${fileNameWithoutExt}_${extWithoutDot}.dae`,\n config: config,\n options: config.options,\n pipeline_start_time: pipeline_start_time,\n pipeline_start_timestamp: pipeline_start_timestamp\n };\n});\n\nconsole.log('Processed files:', files.map(f => f.file_name));\n\nreturn [{\n json: {\n files: files,\n total_files: files.length,\n message: `Found ${files.length} files to convert`,\n config: config,\n pipeline_start_time: pipeline_start_time,\n pipeline_start_timestamp: pipeline_start_timestamp\n }\n}];"
},
"typeVersion": 2
},
{
"id": "357d21f1-ff0b-4308-99a7-8ab3140b6dde",
"name": "파일 존재 여부 확인1",
"type": "n8n-nodes-base.if",
"position": [
1328,
576
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "files-exist-condition",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.files && $json.files.length || 0 }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2
},
{
"id": "84eff3b1-f5a8-4c84-82cb-2fa1d1bc23ea",
"name": "처리를 위한 파일 분할1",
"type": "n8n-nodes-base.splitOut",
"position": [
1520,
560
],
"parameters": {
"options": {},
"fieldToSplitOut": "files"
},
"typeVersion": 1
},
{
"id": "0330e04c-9572-439d-b09f-aa932b5e81ad",
"name": "출력 디렉토리 생성1",
"type": "n8n-nodes-base.executeCommand",
"position": [
1696,
656
],
"parameters": {
"command": "=mkdir \"{{ $json.config.output_folder }}\" 2>nul || echo Output directory ready",
"executeOnce": false
},
"typeVersion": 1
},
{
"id": "1cbf1ee9-0710-4145-a54e-0c4c73e177a6",
"name": "파일 데이터 병합1",
"type": "n8n-nodes-base.merge",
"position": [
1856,
576
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"typeVersion": 3
},
{
"id": "627097e5-5d07-4386-a9d7-22dffe7ba4ab",
"name": "시작 시간 캡처1",
"type": "n8n-nodes-base.code",
"position": [
2032,
576
],
"parameters": {
"jsCode": "// Capture conversion start time and preserve ALL data including pipeline start\nreturn $input.all().map(item => {\n const json = {...item.json};\n json.conversion_start_time = new Date().toISOString();\n json.conversion_start_timestamp = Date.now();\n console.log(`Starting conversion for: ${json.file_name} at ${json.conversion_start_time}`);\n console.log(`Pipeline started at: ${json.pipeline_start_time}`);\n return {json};\n});"
},
"typeVersion": 2
},
{
"id": "edf42bdd-67ed-46d1-a522-cbc93538d894",
"name": "짧은 지연",
"type": "n8n-nodes-base.wait",
"position": [
2192,
576
],
"webhookId": "5a1cb608-ec3e-4832-a289-f6a52ef8bc7a",
"parameters": {
"unit": "milliseconds"
},
"typeVersion": 1
},
{
"id": "1c4e14d2-f9cc-4b4f-85f0-7c0245c32b18",
"name": "변환 실행1",
"type": "n8n-nodes-base.executeCommand",
"position": [
2352,
688
],
"parameters": {
"command": "=\"{{ $json.converter_path }}\" \"{{ $json.file_path }}\" \"{{ $json.expected_output }}\" \"{{ $json.expected_output_dae }}\" {{ $json.options }}",
"executeOnce": false
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "f02f9f74-9b20-4857-92c3-4b0f4103f1b1",
"name": "변환 시간 계산1",
"type": "n8n-nodes-base.code",
"position": [
2512,
688
],
"parameters": {
"jsCode": "// Calculate actual conversion time and rename outputs - preserve pipeline start time\nreturn $input.all().map(item => {\n const json = {...item.json};\n \n // Rename stdout/stderr\n json.conversion_stdout = json.stdout || '';\n json.conversion_stderr = json.stderr || '';\n json.conversion_exitCode = json.exitCode || 0;\n delete json.stdout;\n delete json.stderr;\n delete json.exitCode;\n \n // Calculate actual processing time with decimal precision\n if (json.conversion_start_timestamp) {\n const endTime = Date.now();\n const startTime = json.conversion_start_timestamp;\n const processingTimeMs = endTime - startTime;\n json.processing_time = processingTimeMs / 1000;\n json.processing_time_ms = processingTimeMs;\n json.conversion_end_time = new Date().toISOString();\n json.conversion_end_timestamp = endTime;\n console.log(`Conversion completed for: ${json.file_name} in ${json.processing_time.toFixed(2)} seconds`);\n } else {\n json.processing_time = 0;\n json.processing_time_ms = 0;\n console.warn(`No start timestamp found for: ${json.file_name}`);\n }\n \n // Ensure pipeline start time is preserved\n console.log(`Pipeline start preserved: ${json.pipeline_start_timestamp}`);\n \n return {json};\n});"
},
"typeVersion": 2
},
{
"id": "9255e34b-be94-4630-8887-fc7a0aafd739",
"name": "검증 전 데이터 병합1",
"type": "n8n-nodes-base.merge",
"position": [
2672,
576
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"typeVersion": 3
},
{
"id": "0e3dc51e-4891-4094-ad20-341424dd04df",
"name": "출력 파일 검증 및 크기 확인1",
"type": "n8n-nodes-base.executeCommand",
"position": [
2832,
576
],
"parameters": {
"command": "=powershell -Command \"$xlsx='{{ $json.expected_output }}'; $dae='{{ $json.expected_output_dae }}'; $revit='{{ $json.file_path }}'; $xlsxExists=Test-Path -LiteralPath $xlsx; $daeExists=Test-Path -LiteralPath $dae; $revitExists=Test-Path -LiteralPath $revit; $xlsxSize=if($xlsxExists){(Get-Item -LiteralPath $xlsx).Length}else{0}; $daeSize=if($daeExists){(Get-Item -LiteralPath $dae).Length}else{0}; $revitSize=if($revitExists){(Get-Item -LiteralPath $revit).Length}else{0}; Write-Output ('XLSX_EXISTS:'+$xlsxExists+'|XLSX_SIZE:'+$xlsxSize+'|DAE_EXISTS:'+$daeExists+'|DAE_SIZE:'+$daeSize+'|REVIT_EXISTS:'+$revitExists+'|REVIT_SIZE:'+$revitSize)\"",
"executeOnce": false
},
"typeVersion": 1
},
{
"id": "8d038788-44e7-49be-a999-b97ac4f96fac",
"name": "파일 검증 완료1",
"type": "n8n-nodes-base.code",
"position": [
2992,
576
],
"parameters": {
"jsCode": "// Complete file verification with processing time - preserve pipeline start time\nconst allInputs = $input.all();\nconst mergedDataItems = $('Merge Data Before Verification1').all();\n\nconsole.log(`=== Complete File Verification ===`);\nconsole.log(`Total verification inputs: ${allInputs.length}`);\nconsole.log(`Total merged data items: ${mergedDataItems.length}`);\n\nconst results = [];\n\n// Process each file\nallInputs.forEach((input, index) => {\n const verificationData = input.json || {};\n \n // Find corresponding merged data by matching index\n const mergedData = mergedDataItems[index]?.json || {};\n \n // Extract all necessary data from merged node\n const fileName = mergedData.file_name || 'unknown_file';\n const filePath = mergedData.file_path || '';\n const expectedOutput = mergedData.expected_output || '';\n const expectedOutputDae = mergedData.expected_output_dae || '';\n const fileIndex = mergedData.index || index + 1;\n const processingTime = mergedData.processing_time || 0;\n const config = mergedData.config || {};\n const conversionStartTime = mergedData.conversion_start_time || '';\n const conversionEndTime = mergedData.conversion_end_time || '';\n const conversionEndTimestamp = mergedData.conversion_end_timestamp || Date.now();\n \n // IMPORTANT: Preserve pipeline start time\n const pipelineStartTime = mergedData.pipeline_start_time || '';\n const pipelineStartTimestamp = mergedData.pipeline_start_timestamp || 0;\n\n console.log(`Processing verification for file ${fileIndex}: ${fileName}`);\n console.log('Actual processing time:', processingTime, 'seconds');\n console.log('Pipeline start timestamp:', pipelineStartTimestamp);\n\n // Get verification output from the current input\n let verificationOutput = verificationData.stdout || verificationData.verification_output || '';\n\n // Parse PowerShell output\n let successXlsx = false;\n let successDae = false;\n let successRevit = false;\n let fileSizeXlsx = 0;\n let fileSizeDae = 0;\n let fileSizeRevit = 0;\n\n try {\n if (verificationOutput && verificationOutput.includes('|')) {\n const parts = verificationOutput.split('|');\n \n parts.forEach(part => {\n if (part && part.includes('XLSX_EXISTS:')) {\n successXlsx = part.includes('XLSX_EXISTS:True');\n }\n if (part && part.includes('XLSX_SIZE:')) {\n const sizeStr = part.replace('XLSX_SIZE:', '').trim();\n fileSizeXlsx = parseInt(sizeStr) || 0;\n }\n if (part && part.includes('DAE_EXISTS:')) {\n successDae = part.includes('DAE_EXISTS:True');\n }\n if (part && part.includes('DAE_SIZE:')) {\n const sizeStr = part.replace('DAE_SIZE:', '').trim();\n fileSizeDae = parseInt(sizeStr) || 0;\n }\n if (part && part.includes('REVIT_EXISTS:')) {\n successRevit = part.includes('REVIT_EXISTS:True');\n }\n if (part && part.includes('REVIT_SIZE:')) {\n const sizeStr = part.replace('REVIT_SIZE:', '').trim();\n fileSizeRevit = parseInt(sizeStr) || 0;\n }\n });\n }\n } catch (error) {\n console.log('Error parsing verification output:', error);\n }\n\n const finalSuccess = successXlsx && successDae && fileSizeXlsx > 0 && fileSizeDae > 0;\n\n const statusMessage = finalSuccess \n ? `✅ [${fileIndex}] Successfully converted: ${fileName} (XLSX: ${Math.round(fileSizeXlsx/1024)}KB, DAE: ${Math.round(fileSizeDae/1024)}KB, ${processingTime.toFixed(1)}s)`\n : `❌ [${fileIndex}] Failed to convert: ${fileName} (XLSX: ${successXlsx && fileSizeXlsx > 0 ? '✓' : '✗'}, DAE: ${successDae && fileSizeDae > 0 ? '✓' : '✗'}, ${processingTime.toFixed(1)}s)`;\n\n const result = {\n file_name: fileName,\n file_path: filePath,\n expected_output: expectedOutput,\n expected_output_dae: expectedOutputDae,\n success: finalSuccess,\n success_xlsx: successXlsx && fileSizeXlsx > 0,\n success_dae: successDae && fileSizeDae > 0,\n processing_time: processingTime,\n file_size: fileSizeXlsx + fileSizeDae,\n file_size_xlsx: fileSizeXlsx,\n file_size_dae: fileSizeDae,\n file_size_revit: fileSizeRevit,\n index: fileIndex,\n status: finalSuccess ? 'converted' : 'failed',\n message: statusMessage,\n timestamp: new Date().toISOString(),\n verification_output: verificationOutput,\n config: config,\n conversion_start_time: conversionStartTime,\n conversion_end_time: conversionEndTime,\n conversion_end_timestamp: conversionEndTimestamp,\n pipeline_start_time: pipelineStartTime,\n pipeline_start_timestamp: pipelineStartTimestamp\n };\n\n console.log(`Verification complete for ${fileName}: ${result.message}`);\n results.push({ json: result });\n});\n\nconsole.log(`=== Returning ${results.length} verification results ===`);\nreturn results;"
},
"typeVersion": 2
},
{
"id": "44336609-1655-4f5b-92b4-9b6165620036",
"name": "HTML 보고서 생성1",
"type": "n8n-nodes-base.code",
"position": [
3152,
576
],
"parameters": {
"jsCode": "// Generate HTML Report with CORRECT pipeline timing and no-collada handling\nconst results = $input.all();\n\n// Get the current time as pipeline end\nconst pipelineEndTime = Date.now();\n\n// Find the pipeline start timestamp from the data\nlet pipelineStartTimestamp = null;\nlet config = {};\n\n// Extract timing and config from results\nfor (const result of results) {\n if (result.json) {\n // Get pipeline start timestamp\n if (result.json.pipeline_start_timestamp && !pipelineStartTimestamp) {\n pipelineStartTimestamp = result.json.pipeline_start_timestamp;\n console.log('Found pipeline start timestamp:', pipelineStartTimestamp);\n }\n \n // Get config\n if (result.json.config && Object.keys(result.json.config).length > 0) {\n config = result.json.config;\n }\n }\n}\n\n// Check if no-collada option is enabled\nconst noColladaEnabled = config.options && config.options.includes('no-collada');\nconsole.log('No-collada mode enabled:', noColladaEnabled);\n\n// Calculate total pipeline time in minutes\nlet totalPipelineMinutes = '0.00';\nif (pipelineStartTimestamp) {\n const totalMs = pipelineEndTime - pipelineStartTimestamp;\n totalPipelineMinutes = (totalMs / 60000).toFixed(2);\n console.log('Pipeline timing:', {\n start: pipelineStartTimestamp,\n end: pipelineEndTime,\n totalMs: totalMs,\n totalMinutes: totalPipelineMinutes\n });\n} else {\n console.warn('No pipeline start timestamp found!');\n}\n\n// Use config output folder or fallback\nconst outputFolder = config.output_folder || 'C:\\\\temp';\n\n// Count successful and failed conversions\nlet totalFiles = 0;\nlet successfulConversions = 0;\nlet failedConversions = 0;\nlet totalInputSize = 0;\nlet totalOutputSize = 0;\n\nconst successfulFiles = [];\nconst failedFiles = [];\n\nresults.forEach((result, index) => {\n const data = result.json || {};\n totalFiles++;\n \n const processingTime = data.processing_time || 0;\n const xlsxSuccess = data.success_xlsx || false;\n const daeSuccess = data.success_dae || false;\n const fileSizeRevit = data.file_size_revit || 0;\n const fileSizeXlsx = data.file_size_xlsx || 0;\n const fileSizeDae = data.file_size_dae || 0;\n \n totalInputSize += fileSizeRevit;\n \n // Modified success logic for no-collada mode\n let isSuccess = false;\n if (noColladaEnabled) {\n // In no-collada mode, only XLSX matters\n isSuccess = xlsxSuccess;\n if (isSuccess) {\n totalOutputSize += fileSizeXlsx;\n }\n } else {\n // Normal mode: both XLSX and DAE must succeed\n isSuccess = xlsxSuccess && daeSuccess;\n if (isSuccess) {\n totalOutputSize += fileSizeXlsx + fileSizeDae;\n }\n }\n \n if (isSuccess) {\n successfulConversions++;\n successfulFiles.push({\n name: data.file_name || `File ${index + 1}`,\n originalSize: Math.round(fileSizeRevit / 1024),\n xlsxSize: Math.round(fileSizeXlsx / 1024),\n daeSize: Math.round(fileSizeDae / 1024),\n totalSize: Math.round((fileSizeXlsx + (noColladaEnabled ? 0 : fileSizeDae)) / 1024),\n xlsxPath: data.expected_output || '',\n daePath: data.expected_output_dae || '',\n noCollada: noColladaEnabled\n });\n } else {\n failedConversions++;\n failedFiles.push({\n name: data.file_name || `File ${index + 1}`,\n originalSize: Math.round(fileSizeRevit / 1024),\n xlsxStatus: xlsxSuccess ? '✓' : '✗',\n daeStatus: daeSuccess ? '✓' : '✗'\n });\n }\n});\n\nconst successRate = totalFiles > 0 ? Math.round((successfulConversions / totalFiles) * 100) : 0;\n\n// Generate timestamp for filename\nconst now = new Date();\nconst timestamp = now.toISOString().slice(0,19).replace(/:/g, '-');\nconst fileName = `CAD_Conversion_Report_${timestamp}.html`;\nconst fullPath = `${outputFolder}\\\\${fileName}`.replace(/\\\\\\\\/g, '\\\\');\n\nconsole.log('Report summary:', {\n totalFiles,\n successfulConversions,\n failedConversions,\n successRate: successRate + '%',\n totalPipelineTime: totalPipelineMinutes + ' minutes',\n outputPath: fullPath,\n noColladaMode: noColladaEnabled\n});\n\n// Create HTML content with professional styling\nconst htmlContent = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>CAD Project Batch Conversion Report</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n body { \n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; \n background: #f5f6fa;\n color: #2c3e50;\n line-height: 1.6;\n }\n \n .container { \n max-width: 1400px; \n margin: 0 auto; \n background: white; \n box-shadow: 0 0 40px rgba(0, 0, 0, 0.08); \n }\n \n .header { \n background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);\n color: white; \n padding: 60px 40px;\n position: relative;\n overflow: hidden;\n }\n \n .header::before {\n content: '';\n position: absolute;\n top: -50%;\n right: -10%;\n width: 40%;\n height: 200%;\n background: rgba(255, 255, 255, 0.05);\n transform: rotate(35deg);\n }\n \n .header-content {\n position: relative;\n z-index: 1;\n }\n \n .header h1 { \n font-size: 2.8rem; \n font-weight: 300;\n letter-spacing: -1px;\n margin-bottom: 15px;\n }\n \n .header .subtitle { \n font-size: 1.1rem;\n opacity: 0.85;\n font-weight: 400;\n }\n \n .header .timestamp {\n margin-top: 20px;\n font-size: 0.95rem;\n opacity: 0.7;\n }\n \n .content {\n padding: 40px;\n }\n \n .metrics { \n display: grid; \n grid-template-columns: repeat(5, 1fr); \n gap: 20px; \n margin: -20px 0 40px 0;\n position: relative;\n z-index: 10;\n }\n \n @media (max-width: 1200px) {\n .metrics {\n grid-template-columns: repeat(3, 1fr);\n }\n }\n \n @media (max-width: 768px) {\n .metrics {\n grid-template-columns: repeat(2, 1fr);\n }\n }\n \n @media (max-width: 480px) {\n .metrics {\n grid-template-columns: 1fr;\n }\n }\n \n .metric { \n background: white;\n padding: 30px 20px;\n border-radius: 12px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\n transition: all 0.3s ease;\n border: 1px solid #e8ecf1;\n position: relative;\n overflow: hidden;\n text-align: center;\n }\n \n .metric::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 4px;\n background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);\n }\n \n .metric:hover {\n transform: translateY(-3px);\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\n }\n \n .metric-value { \n font-size: 2.2rem; \n font-weight: 600;\n color: #1e3c72;\n margin-bottom: 8px;\n line-height: 1;\n }\n \n .metric-label { \n font-size: 0.85rem; \n color: #64748b;\n font-weight: 500;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n \n .metric.highlight {\n background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);\n border-color: #3b82f6;\n }\n \n .metric.highlight::before {\n background: linear-gradient(90deg, #10b981 0%, #059669 100%);\n }\n \n .section { \n margin: 40px 0;\n background: white;\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 2px 15px rgba(0, 0, 0, 0.06);\n border: 1px solid #e8ecf1;\n }\n \n .section-header {\n background: #f8fafc;\n padding: 24px 32px;\n border-bottom: 1px solid #e8ecf1;\n }\n \n .section h2 { \n color: #1e293b;\n font-size: 1.4rem;\n font-weight: 600;\n display: flex;\n align-items: center;\n gap: 10px;\n }\n \n .section-content {\n padding: 0;\n }\n \n table { \n width: 100%; \n border-collapse: collapse;\n }\n \n th, td { \n padding: 16px 24px;\n text-align: left;\n border-bottom: 1px solid #f1f5f9;\n }\n \n th { \n background: #f8fafc;\n font-weight: 600;\n color: #475569;\n font-size: 0.875rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n position: sticky;\n top: 0;\n z-index: 1;\n }\n \n tr:last-child td {\n border-bottom: none;\n }\n \n tr:hover { \n background: #f8fafc;\n }\n \n .status-success { \n background: #10b981;\n color: white;\n padding: 6px 12px;\n border-radius: 6px;\n font-size: 0.75rem;\n font-weight: 600;\n display: inline-block;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n \n .status-failed { \n background: #ef4444;\n color: white;\n padding: 6px 12px;\n border-radius: 6px;\n font-size: 0.75rem;\n font-weight: 600;\n display: inline-block;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n \n .status-skipped { \n background: #6b7280;\n color: white;\n padding: 6px 12px;\n border-radius: 6px;\n font-size: 0.75rem;\n font-weight: 600;\n display: inline-block;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n \n .config-section {\n background: #f8fafc;\n padding: 32px;\n border-radius: 12px;\n margin: 40px 0;\n }\n \n .config-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));\n gap: 24px;\n margin-top: 20px;\n }\n \n .config-item {\n display: flex;\n align-items: flex-start;\n gap: 16px;\n }\n \n .config-label {\n font-weight: 600;\n color: #475569;\n min-width: 140px;\n font-size: 0.9rem;\n }\n \n .config-value {\n color: #1e293b;\n font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;\n background: white;\n padding: 8px 12px;\n border-radius: 6px;\n font-size: 0.85rem;\n word-break: break-all;\n flex: 1;\n border: 1px solid #e2e8f0;\n }\n \n .footer { \n background: #f8fafc;\n padding: 40px;\n text-align: center;\n color: #64748b;\n border-top: 1px solid #e8ecf1;\n }\n \n .footer .company {\n font-size: 1.1rem;\n font-weight: 600;\n color: #1e293b;\n margin-bottom: 8px;\n }\n \n .footer .version {\n font-size: 0.85rem;\n margin-top: 16px;\n color: #94a3b8;\n }\n \n .report-location {\n background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);\n border: 1px solid #3b82f6;\n border-radius: 12px;\n padding: 20px 28px;\n margin: 30px 0;\n text-align: center;\n color: #1e3c72;\n font-size: 0.95rem;\n }\n \n .report-location strong {\n color: #1e3c72;\n font-weight: 600;\n }\n \n .size-cell {\n font-family: 'SF Mono', Monaco, monospace;\n font-size: 0.9rem;\n }\n \n .file-link {\n color: #3b82f6;\n text-decoration: none;\n font-weight: 500;\n transition: all 0.2s ease;\n position: relative;\n display: inline-block;\n padding: 2px 0;\n }\n \n .file-link:hover {\n color: #2563eb;\n text-decoration: underline;\n }\n \n .tooltip {\n position: relative;\n display: inline-block;\n }\n \n .tooltip .tooltiptext {\n visibility: hidden;\n width: 280px;\n background-color: #333;\n color: #fff;\n text-align: center;\n border-radius: 8px;\n padding: 12px 16px;\n position: absolute;\n z-index: 1000;\n bottom: 125%;\n left: 50%;\n margin-left: -140px;\n opacity: 0;\n transition: opacity 0.3s;\n font-size: 0.85rem;\n line-height: 1.4;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n }\n \n .tooltip .tooltiptext::after {\n content: \"\";\n position: absolute;\n top: 100%;\n left: 50%;\n margin-left: -5px;\n border-width: 5px;\n border-style: solid;\n border-color: #333 transparent transparent transparent;\n }\n \n .tooltip:hover .tooltiptext {\n visibility: visible;\n opacity: 1;\n }\n \n .no-collada-notice {\n background: #fef3c7;\n border: 1px solid #fbbf24;\n border-radius: 8px;\n padding: 16px;\n margin: 20px 0;\n color: #92400e;\n font-size: 0.9rem;\n text-align: center;\n }\n \n .no-collada-notice strong {\n color: #78350f;\n }\n \n @media print {\n body { background: white; }\n .container { box-shadow: none; }\n .metric { box-shadow: none; border: 1px solid #e5e7eb; }\n .section { box-shadow: none; page-break-inside: avoid; }\n .tooltip .tooltiptext { display: none; }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <div class=\"header-content\">\n <h1>CAD Project Batch Conversion Report</h1>\n <p class=\"subtitle\">Automated conversion pipeline execution summary</p>\n <p class=\"timestamp\">${now.toLocaleString('en-US', { \n weekday: 'long', \n year: 'numeric', \n month: 'long', \n day: 'numeric', \n hour: '2-digit', \n minute: '2-digit' \n })}</p>\n </div>\n </div>\n \n <div class=\"content\">\n <div class=\"metrics\">\n <div class=\"metric\">\n <div class=\"metric-value\">${totalFiles}</div>\n <div class=\"metric-label\">Total Files</div>\n </div>\n <div class=\"metric highlight\">\n <div class=\"metric-value\">${successRate}%</div>\n <div class=\"metric-label\">Success Rate</div>\n </div>\n <div class=\"metric\">\n <div class=\"metric-value\">${totalPipelineMinutes}</div>\n <div class=\"metric-label\">Total Time (Minutes)</div>\n </div>\n <div class=\"metric\">\n <div class=\"metric-value\">${Math.round(totalInputSize / 1048576)} MB</div>\n <div class=\"metric-label\">Total Input Size</div>\n </div>\n <div class=\"metric\">\n <div class=\"metric-value\">${Math.round(totalOutputSize / 1048576)} MB</div>\n <div class=\"metric-label\">Total Output Size</div>\n </div>\n </div>\n\n <div class=\"report-location\">\n <strong>Report Location:</strong> ${fullPath}\n </div>\n\n ${noColladaEnabled ? `\n <div class=\"no-collada-notice\">\n <strong>Note:</strong> DAE (Collada) file generation was skipped as per configuration (no-collada option enabled).\n </div>\n ` : ''}\n\n ${successfulFiles.length > 0 ? `\n <div class=\"section\">\n <div class=\"section-header\">\n <h2>✅ Successfully Converted Files (${successfulFiles.length})</h2>\n </div>\n <div class=\"section-content\">\n <table>\n <thead>\n <tr>\n <th>File Name</th>\n <th>Original Size</th>\n <th>Excel Output</th>\n ${noColladaEnabled ? '<th>DAE Output</th>' : '<th>DAE Output</th>'}\n <th>Total Output</th>\n </tr>\n </thead>\n <tbody>\n ${successfulFiles.map(file => `\n <tr>\n <td><strong>${file.name}</strong></td>\n <td class=\"size-cell\">${file.originalSize} KB</td>\n <td class=\"size-cell\">\n <a href=\"file:///${file.xlsxPath.replace(/\\\\/g, '/')}\" class=\"file-link\" target=\"_blank\">${file.xlsxSize} KB</a>\n </td>\n <td class=\"size-cell\">\n ${noColladaEnabled ? \n '<span class=\"status-skipped\">SKIPPED</span>' : \n `<div class=\"tooltip\">\n <a href=\"file:///${file.daePath.replace(/\\\\/g, '/')}\" class=\"file-link\" target=\"_blank\">${file.daeSize} KB</a>\n <span class=\"tooltiptext\">💡 Tip: Use the free CAD Assistant software for the best viewing experience of DAE files</span>\n </div>`\n }\n </td>\n <td class=\"size-cell\"><strong>${file.totalSize} KB</strong></td>\n </tr>\n `).join('')}\n </tbody>\n </table>\n </div>\n </div>\n ` : ''}\n\n ${failedFiles.length > 0 ? `\n <div class=\"section\">\n <div class=\"section-header\">\n <h2>❌ Failed Conversions (${failedFiles.length})</h2>\n </div>\n <div class=\"section-content\">\n <table>\n <thead>\n <tr>\n <th>File Name</th>\n <th>Original Size</th>\n <th>XLSX Status</th>\n <th>DAE Status</th>\n </tr>\n </thead>\n <tbody>\n ${failedFiles.map(file => `\n <tr>\n <td><strong>${file.name}</strong></td>\n <td class=\"size-cell\">${file.originalSize} KB</td>\n <td><span class=\"status-${file.xlsxStatus === '✓' ? 'success' : 'failed'}\">${file.xlsxStatus === '✓' ? 'SUCCESS' : 'FAILED'}</span></td>\n <td>${noColladaEnabled && file.xlsxStatus === '✓' ? \n '<span class=\"status-skipped\">SKIPPED</span>' : \n `<span class=\"status-${file.daeStatus === '✓' ? 'success' : 'failed'}\">${file.daeStatus === '✓' ? 'SUCCESS' : 'FAILED'}</span>`\n }</td>\n </tr>\n `).join('')}\n </tbody>\n </table>\n </div>\n </div>\n ` : ''}\n\n <div class=\"config-section\">\n <h2 style=\"color: #1e293b; font-size: 1.4rem; font-weight: 600; margin-bottom: 20px;\">\n ⚙️ Configuration Details\n </h2>\n <div class=\"config-grid\">\n <div class=\"config-item\">\n <span class=\"config-label\">Source Folder:</span>\n <span class=\"config-value\">${config.source_folder || 'Not specified'}</span>\n </div>\n <div class=\"config-item\">\n <span class=\"config-label\">Output Folder:</span>\n <span class=\"config-value\">${config.output_folder || 'Not specified'}</span>\n </div>\n <div class=\"config-item\">\n <span class=\"config-label\">File Extension:</span>\n <span class=\"config-value\">${config.file_extension || 'Not specified'}</span>\n </div>\n <div class=\"config-item\">\n <span class=\"config-label\">Include Subfolders:</span>\n <span class=\"config-value\">${config.include_subfolders ? 'Yes' : 'No'}</span>\n </div>\n <div class=\"config-item\">\n <span class=\"config-label\">Converter Options:</span>\n <span class=\"config-value\">${config.options || 'None'}</span>\n </div>\n <div class=\"config-item\">\n <span class=\"config-label\">Generated:</span>\n <span class=\"config-value\">${now.toLocaleString()}</span>\n </div>\n <div class=\"config-item\">\n <span class=\"config-label\">Workflow ID:</span>\n <span class=\"config-value\">FYFQhblt4gILLSpe</span>\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"footer\">\n <div class=\"company\">DataDrivenConstruction.io</div>\n <div>Professional CAD Conversion Pipeline</div>\n <div class=\"version\">Version 2.0 | Automated Batch Processing System</div>\n </div>\n </div>\n</body>\n</html>`;\n\n// Return data for saving and opening\nreturn [{\n json: {\n html_content: htmlContent,\n file_name: fileName,\n full_path: fullPath,\n output_folder: outputFolder,\n total_files: totalFiles,\n successful_conversions: successfulConversions,\n failed_conversions: failedConversions,\n success_rate: successRate,\n total_pipeline_time: totalPipelineMinutes,\n config: config,\n summary_message: `✅ Conversion Complete! ${successfulConversions}/${totalFiles} files converted successfully (${successRate}% success rate) in ${totalPipelineMinutes} minutes`\n }\n}];"
},
"typeVersion": 2
},
{
"id": "21c9bd6d-e1a0-4697-b99d-c51e666af986",
"name": "바이너리 데이터 준비1",
"type": "n8n-nodes-base.code",
"position": [
3312,
576
],
"parameters": {
"jsCode": "// Prepare binary data for file saving\nconst item = $input.first().json;\n\nconsole.log('Preparing binary data for file:', item.file_name);\nconsole.log('Full path:', item.full_path);\n\nif (!item.html_content) {\n throw new Error('No HTML content found');\n}\n\nreturn [{\n json: item,\n binary: {\n report: {\n data: Buffer.from(item.html_content, 'utf-8').toString('base64'),\n mimeType: 'text/html',\n fileName: item.file_name,\n fileExtension: 'html'\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "3ba8b465-5da4-4b79-83e2-2bdb63222b6b",
"name": "HTML 파일 저장1",
"type": "n8n-nodes-base.writeBinaryFile",
"position": [
3472,
576
],
"parameters": {
"options": {},
"fileName": "={{ $json.full_path }}",
"dataPropertyName": "report"
},
"typeVersion": 1
},
{
"id": "94de0bbb-402b-4dea-bcaf-f98fdc678048",
"name": "경로 검증 및 준비1",
"type": "n8n-nodes-base.code",
"position": [
3632,
576
],
"parameters": {
"jsCode": "// Verify file was saved and prepare for opening\nconst data = $input.first().json;\nconst fullPath = data.full_path || '';\n\nconsole.log('Preparing to open file:', fullPath);\n\n// Normalize path for Windows\nconst windowsPath = fullPath.replace(/\\\\\\\\/g, '\\\\').replace(/\\//g, '\\\\');\n\nreturn [{\n json: {\n ...data,\n windows_path: windowsPath,\n command_to_open: `cmd /c start \"\" \"${windowsPath}\"`\n }\n}];"
},
"typeVersion": 2
},
{
"id": "6558ac19-23c1-44c9-8b7b-3615052cd4a5",
"name": "HTML 보고서 열기1",
"type": "n8n-nodes-base.executeCommand",
"position": [
3792,
576
],
"parameters": {
"command": "=cmd /c start \"\" \"{{ $json.windows_path }}\""
},
"typeVersion": 1
},
{
"id": "cdf12580-ade1-4ae0-a699-980aca156c98",
"name": "최종 완료 알림1",
"type": "n8n-nodes-base.executeCommand",
"position": [
3952,
576
],
"parameters": {
"command": "=echo 🏁 CAD Conversion Pipeline Completed Successfully! & echo 📋 Summary: {{ $json.summary_message }} & echo 📁 Report Location: {{ $json.windows_path }} & echo ✨ All processes finished successfully! & echo. & echo 🎯 Next Steps: & echo 1. Check the HTML report that opened in your browser & echo 2. Verify output files in the specified folder & echo 3. Review conversion results and metrics & echo. & echo ✅ Pipeline execution completed!",
"executeOnce": false
},
"typeVersion": 1
},
{
"id": "dc253ac4-c296-4a8c-897c-380e5e14b683",
"name": "파일 없음 응답1",
"type": "n8n-nodes-base.set",
"position": [
1520,
784
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "no-files-message",
"name": "message",
"type": "string",
"value": "⚠️ No files found to convert with the specified parameters"
},
{
"id": "no-files-details",
"name": "details",
"type": "string",
"value": "Please check: 1) Source folder path exists, 2) File extension is correct, 3) Files exist in the specified location"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "ee8bb887-c553-412b-9be5-c865f08fbd82",
"name": "고정 메모7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-144,
112
],
"parameters": {
"color": 6,
"width": 756,
"height": 844,
"content": "## 🏁 GROUP 1: INITIALIZATION\n\n**Nodes in this group:**\n• Manual Trigger\n• Capture Pipeline Start Time\n• Set Configuration Parameters\n• Merge Pipeline Start with Config\n\n**What happens here:**\n1️⃣ Pipeline starts manually or on schedule\n2️⃣ Current timestamp is captured for metrics\n3️⃣ All settings are loaded (paths, folders, etc.)\n4️⃣ Configuration merged with timing data\n\n💡 **Key:** This sets up everything needed for the conversion process"
},
"typeVersion": 1
},
{
"id": "0ca6f619-b073-41b1-a248-0f2b62333773",
"name": "고정 메모9",
"type": "n8n-nodes-base.stickyNote",
"position": [
624,
112
],
"parameters": {
"color": 5,
"width": 1024,
"height": 840,
"content": "## 🔍 GROUP 2: FILE DISCOVERY\n\n**Nodes in this group:**\n• Find CAD Files\n• Merge Config with Search Results\n• Process File List\n• Check if Files Exist\n\n**What happens here:**\n1️⃣ PowerShell scans the source folder\n2️⃣ Filters files by extension (.rvt, .ifc, etc.)\n3️⃣ Creates a list with full paths\n4️⃣ Validates that files were found\n\n📁 **Output:** Array of file objects with paths\n❌ **If no files:** Pipeline stops gracefully"
},
"typeVersion": 1
},
{
"id": "8dcff587-4f09-4e86-a065-bdd056be456d",
"name": "고정 메모10",
"type": "n8n-nodes-base.stickyNote",
"position": [
1664,
112
],
"parameters": {
"color": 5,
"width": 316,
"height": 844,
"content": "## 🔄 GROUP 3: BATCH PREPARATION\n\n**Nodes in this group:**\n• Split Files for Processing\n• Create Output Directory\n• Merge File Data\n\n**What happens here:**\n1️⃣ File list is split into individual items\n2️⃣ Output directory is created (if needed)\n3️⃣ Each file gets conversion parameters\n\n🎯 **Purpose:** Enables parallel processing\n📊 **Data added:** Expected output paths for XLSX & DAE"
},
"typeVersion": 1
},
{
"id": "ae685ee4-bbee-4b56-85e0-26f3da9053fd",
"name": "고정 메모11",
"type": "n8n-nodes-base.stickyNote",
"position": [
2000,
112
],
"parameters": {
"color": 5,
"width": 800,
"height": 840,
"content": "## ⚡ GROUP 4: CONVERSION EXECUTION\n\n**Nodes in this group:**\n• Capture Start Time\n• Small Delay\n• Execute Conversion\n• Calculate Conversion Time\n\n**What happens here:**\n1️⃣ Timestamp captured for each file\n2️⃣ Small delay prevents overload\n3️⃣ RvtExporter.exe runs the conversion\n4️⃣ Processing time calculated\n\n🔧 **Command:** \n`RvtExporter.exe [input] [output.xlsx] [output.dae]`\n\n⏱️ **Conversion Time:** 1 Minute per 100Mb."
},
"typeVersion": 1
},
{
"id": "5fe7b6d6-7a36-41ae-a431-a93803227a50",
"name": "고정 메모15",
"type": "n8n-nodes-base.stickyNote",
"position": [
2816,
112
],
"parameters": {
"color": 6,
"width": 284,
"height": 836,
"content": "## ✅ GROUP 5: VALIDATION\n\n**Nodes in this group:**\n• Merge Data Before Verification\n• Verify Output Files and Get Sizes\n• Complete File Verification\n\n**What happens here:**\n1️⃣ All conversion data is consolidated\n2️⃣ PowerShell checks if outputs exist\n3️⃣ File sizes are measured\n4️⃣ Success/failure status determined\n\n**Success criteria:**\n• Both XLSX and DAE files exist\n• File sizes > 0 bytes\n• No conversion errors"
},
"typeVersion": 1
},
{
"id": "2a199a8f-7206-4b18-9e87-f9dc4db56d8d",
"name": "고정 메모16",
"type": "n8n-nodes-base.stickyNote",
"position": [
3120,
112
],
"parameters": {
"width": 456,
"height": 832,
"content": "## 📊 GROUP 6: REPORTING\n\n**Nodes in this group:**\n• Generate HTML Report\n• Prepare Binary Data\n• Save HTML File\n\n**What happens here:**\n1️⃣ All results compiled into statistics\n2️⃣ Professional HTML report generated\n3️⃣ Report saved to output folder\n\n**Report includes:**\n• Success rate & metrics\n• Processing times\n• File sizes & links\n• Detailed conversion log"
},
"typeVersion": 1
},
{
"id": "565d9855-8447-4c4e-ab57-b652a8733040",
"name": "고정 메모17",
"type": "n8n-nodes-base.stickyNote",
"position": [
3600,
112
],
"parameters": {
"width": 484,
"height": 828,
"content": "## 🎯 GROUP 7: FINALIZATION\n\n**Nodes in this group:**\n• Verify and Prepare Path\n• Open HTML Report\n• Final Completion Notice\n\n**What happens here:**\n1️⃣ Report path normalized for Windows\n2️⃣ HTML report opens in browser\n3️⃣ Success message displayed\n\n**Final output:**\n• Report auto-opens\n• Summary in console\n• All files ready for use"
},
"typeVersion": 1
},
{
"id": "32324229-f786-4023-bfc1-bc01ad05575d",
"name": "고정 메모30",
"type": "n8n-nodes-base.stickyNote",
"position": [
-144,
976
],
"parameters": {
"color": 4,
"width": 488,
"height": 308,
"content": "## 📝 Configuration Example\n\n```\nconverter_path: \nC:\\DDC_Converter\\RvtExporter.exe\nsource_folder: \nC:\\Projects\\Building_A\noutput_folder: \nC:\\Projects\\Converted\nfile_extension: .rvt\ninclude_subfolders: false\n```"
},
"typeVersion": 1
},
{
"id": "62e0570c-238d-40e2-8b1f-f1f71ca8f034",
"name": "예약 트리거1",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-80,
560
],
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"typeVersion": 1.2
},
{
"id": "637d319f-4c7d-4f14-8676-16e6a839812d",
"name": "고정 메모2",
"type": "n8n-nodes-base.stickyNote",
"position": [
368,
304
],
"parameters": {
"color": 4,
"width": 192,
"height": 384,
"content": "## ⬇️ Only modify the variables here \n— everything else works automatically"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "7eb4a013-6adf-4f71-b5d8-1c78163410dd",
"connections": {
"edf42bdd-67ed-46d1-a522-cbc93538d894": {
"main": [
[
{
"node": "1c4e14d2-f9cc-4b4f-85f0-7c0245c32b18",
"type": "main",
"index": 0
}
]
]
},
"2fe7360d-df50-4eb7-8799-441e7aed7a1a": {
"main": [
[
{
"node": "614d8fe1-6c80-4cb0-b941-5aa3998a2321",
"type": "main",
"index": 0
}
]
]
},
"53648934-0bb7-49d3-83f9-ab4280bb3ec9": {
"main": [
[
{
"node": "894a4fae-cd2f-4418-b35b-fc6a06cda820",
"type": "main",
"index": 1
}
]
]
},
"3ba8b465-5da4-4b79-83e2-2bdb63222b6b": {
"main": [
[
{
"node": "94de0bbb-402b-4dea-bcaf-f98fdc678048",
"type": "main",
"index": 0
}
]
]
},
"1cbf1ee9-0710-4145-a54e-0c4c73e177a6": {
"main": [
[
{
"node": "627097e5-5d07-4386-a9d7-22dffe7ba4ab",
"type": "main",
"index": 0
}
]
]
},
"6558ac19-23c1-44c9-8b7b-3615052cd4a5": {
"main": [
[
{
"node": "cdf12580-ade1-4ae0-a699-980aca156c98",
"type": "main",
"index": 0
}
]
]
},
"cc6f17d1-4393-4c77-947a-53c42c01543c": {
"main": [
[
{
"node": "357d21f1-ff0b-4308-99a7-8ab3140b6dde",
"type": "main",
"index": 0
}
]
]
},
"627097e5-5d07-4386-a9d7-22dffe7ba4ab": {
"main": [
[
{
"node": "edf42bdd-67ed-46d1-a522-cbc93538d894",
"type": "main",
"index": 0
},
{
"node": "9255e34b-be94-4630-8887-fc7a0aafd739",
"type": "main",
"index": 0
}
]
]
},
"1c4e14d2-f9cc-4b4f-85f0-7c0245c32b18": {
"main": [
[
{
"node": "f02f9f74-9b20-4857-92c3-4b0f4103f1b1",
"type": "main",
"index": 0
}
]
]
},
"21c9bd6d-e1a0-4697-b99d-c51e666af986": {
"main": [
[
{
"node": "3ba8b465-5da4-4b79-83e2-2bdb63222b6b",
"type": "main",
"index": 0
}
]
]
},
"357d21f1-ff0b-4308-99a7-8ab3140b6dde": {
"main": [
[
{
"node": "84eff3b1-f5a8-4c84-82cb-2fa1d1bc23ea",
"type": "main",
"index": 0
}
],
[
{
"node": "dc253ac4-c296-4a8c-897c-380e5e14b683",
"type": "main",
"index": 0
}
]
]
},
"44336609-1655-4f5b-92b4-9b6165620036": {
"main": [
[
{
"node": "21c9bd6d-e1a0-4697-b99d-c51e666af986",
"type": "main",
"index": 0
}
]
]
},
"0330e04c-9572-439d-b09f-aa932b5e81ad": {
"main": [
[
{
"node": "1cbf1ee9-0710-4145-a54e-0c4c73e177a6",
"type": "main",
"index": 1
}
]
]
},
"94de0bbb-402b-4dea-bcaf-f98fdc678048": {
"main": [
[
{
"node": "6558ac19-23c1-44c9-8b7b-3615052cd4a5",
"type": "main",
"index": 0
}
]
]
},
"f02f9f74-9b20-4857-92c3-4b0f4103f1b1": {
"main": [
[
{
"node": "9255e34b-be94-4630-8887-fc7a0aafd739",
"type": "main",
"index": 1
}
]
]
},
"8d038788-44e7-49be-a999-b97ac4f96fac": {
"main": [
[
{
"node": "44336609-1655-4f5b-92b4-9b6165620036",
"type": "main",
"index": 0
}
]
]
},
"84eff3b1-f5a8-4c84-82cb-2fa1d1bc23ea": {
"main": [
[
{
"node": "0330e04c-9572-439d-b09f-aa932b5e81ad",
"type": "main",
"index": 0
},
{
"node": "1cbf1ee9-0710-4145-a54e-0c4c73e177a6",
"type": "main",
"index": 0
}
]
]
},
"614d8fe1-6c80-4cb0-b941-5aa3998a2321": {
"main": [
[
{
"node": "7b214d29-a619-4a41-8cfa-67f074470b89",
"type": "main",
"index": 0
},
{
"node": "e64288d1-9d53-4036-afde-437c9892a927",
"type": "main",
"index": 0
}
]
]
},
"7b214d29-a619-4a41-8cfa-67f074470b89": {
"main": [
[
{
"node": "e64288d1-9d53-4036-afde-437c9892a927",
"type": "main",
"index": 1
}
]
]
},
"9255e34b-be94-4630-8887-fc7a0aafd739": {
"main": [
[
{
"node": "0e3dc51e-4891-4094-ad20-341424dd04df",
"type": "main",
"index": 0
}
]
]
},
"894a4fae-cd2f-4418-b35b-fc6a06cda820": {
"main": [
[
{
"node": "cc6f17d1-4393-4c77-947a-53c42c01543c",
"type": "main",
"index": 0
}
]
]
},
"e64288d1-9d53-4036-afde-437c9892a927": {
"main": [
[
{
"node": "53648934-0bb7-49d3-83f9-ab4280bb3ec9",
"type": "main",
"index": 0
},
{
"node": "894a4fae-cd2f-4418-b35b-fc6a06cda820",
"type": "main",
"index": 0
}
]
]
},
"0e3dc51e-4891-4094-ad20-341424dd04df": {
"main": [
[
{
"node": "8d038788-44e7-49be-a999-b97ac4f96fac",
"type": "main",
"index": 0
}
]
]
}
}
}자주 묻는 질문
이 워크플로우를 어떻게 사용하나요?
위의 JSON 구성 코드를 복사하여 n8n 인스턴스에서 새 워크플로우를 생성하고 "JSON에서 가져오기"를 선택한 후, 구성을 붙여넣고 필요에 따라 인증 설정을 수정하세요.
이 워크플로우는 어떤 시나리오에 적합한가요?
고급 - 문서 추출, 멀티모달 AI
유료인가요?
이 워크플로우는 완전히 무료이며 직접 가져와 사용할 수 있습니다. 다만, 워크플로우에서 사용하는 타사 서비스(예: OpenAI API)는 사용자 직접 비용을 지불해야 할 수 있습니다.
관련 워크플로우 추천
n8n_7_Revit와 IFC 탄소 배출 CO2 추산기
AI 분류 계산을 통해 Revit/IFC 모델의 숨겨진 탄소(CO2)을 계산합니다.
If
Set
Code
+
If
Set
Code
55 노드Artem Boiko
AI 요약
n8n_6_ LLM을 사용하여 Revit와 IFC의 건설 비용 추정
GPT-4와 Claude를 사용하여 Revit/IFC 모델을 기반으로 건설 비용을 추정
If
Set
Code
+
If
Set
Code
55 노드Artem Boiko
AI 요약
시각화 참조 라이브러리에서 n8n 노드를 탐색
可视化 참조 라이브러리에서 n8n 노드를 탐색
If
Ftp
Set
+
If
Ftp
Set
113 노드I versus AI
기타
AI-Deepseek-R1t 회사 출장 승인 및 경비 승인 요청
Deepseek AI, Gmail, Google Sheets를 통한 회의 출장 승인 자동화
If
Set
Code
+
If
Set
Code
24 노드Cheng Siong Chin
문서 추출
n8n_8_Revit_IFC_DWG 변환_추출 단계_XLSX 분석
Revit 모델 데이터 추출 및 구조화된 Excel 형식으로 분석
If
Set
Manual Trigger
+
If
Set
Manual Trigger
13 노드Artem Boiko
엔지니어링
완전한 B2B 판매 프로세스: Apollo 잠재 고객 생성, Mailgun 프로모션 및 AI 응답 관리
완전한 B2B 판매 프로세스: Apollo 잠재 고객 생성, Mailgun 확장 및 AI 응답 관리
If
Set
Code
+
If
Set
Code
116 노드Paul
콘텐츠 제작
워크플로우 정보
난이도
고급
노드 수82
카테고리2
노드 유형17
저자
Artem Boiko
@datadrivenconstructionFounder DataDrivenConstruction.io | AEC Tech Consultant & Automation Expert | Bridging Software and Construction
외부 링크
n8n.io에서 보기 →
이 워크플로우 공유