diff --git a/.mcp.json b/.mcp.json
index feef2d2..2dc5dad 100644
--- a/.mcp.json
+++ b/.mcp.json
@@ -7,7 +7,7 @@
"npx",
"-y",
"figma-developer-mcp",
- "--figma-api-key=figd_R6ASvFG2IHcHs35_XFPJh0sTkvp4RxWyEhMhT9vv",
+ "--figma-api-key=figd_s23TfSDL0hS97DIialy0R2P6QsoZQHfuGx1l_t-k",
"--stdio"
]
}
diff --git a/docs/bg-texture-only.png b/docs/bg-texture-only.png
new file mode 100644
index 0000000..eb000f7
Binary files /dev/null and b/docs/bg-texture-only.png differ
diff --git a/docs/figma-analysis/1-1_d1.json b/docs/figma-analysis/1-1_d1.json
new file mode 100644
index 0000000..e761fb7
--- /dev/null
+++ b/docs/figma-analysis/1-1_d1.json
@@ -0,0 +1 @@
+{"status":429,"err":"Rate limit exceeded"}
\ No newline at end of file
diff --git a/docs/figma-analysis/1-1_raw.json b/docs/figma-analysis/1-1_raw.json
new file mode 100644
index 0000000..e761fb7
--- /dev/null
+++ b/docs/figma-analysis/1-1_raw.json
@@ -0,0 +1 @@
+{"status":429,"err":"Rate limit exceeded"}
\ No newline at end of file
diff --git a/docs/figma-analysis/1-1_미래_raw.json b/docs/figma-analysis/1-1_미래_raw.json
new file mode 100644
index 0000000..e761fb7
--- /dev/null
+++ b/docs/figma-analysis/1-1_미래_raw.json
@@ -0,0 +1 @@
+{"status":429,"err":"Rate limit exceeded"}
\ No newline at end of file
diff --git a/docs/figma-screenshots/1-1_미래.png b/docs/figma-screenshots/1-1_미래.png
new file mode 100644
index 0000000..e1fd6f5
Binary files /dev/null and b/docs/figma-screenshots/1-1_미래.png differ
diff --git a/docs/figma-screenshots/2-2장_자세히보기.png b/docs/figma-screenshots/2-2장_자세히보기.png
new file mode 100644
index 0000000..f4a9604
Binary files /dev/null and b/docs/figma-screenshots/2-2장_자세히보기.png differ
diff --git a/docs/figma-screenshots/가치.png b/docs/figma-screenshots/가치.png
new file mode 100644
index 0000000..487368b
Binary files /dev/null and b/docs/figma-screenshots/가치.png differ
diff --git a/docs/figma-screenshots/기술.png b/docs/figma-screenshots/기술.png
new file mode 100644
index 0000000..a9d2223
Binary files /dev/null and b/docs/figma-screenshots/기술.png differ
diff --git a/docs/test-bg-layer.png b/docs/test-bg-layer.png
new file mode 100644
index 0000000..e90738e
Binary files /dev/null and b/docs/test-bg-layer.png differ
diff --git a/docs/test-gemini-infographic.png b/docs/test-gemini-infographic.png
new file mode 100644
index 0000000..abbd701
Binary files /dev/null and b/docs/test-gemini-infographic.png differ
diff --git a/docs/test-layered-slide.html b/docs/test-layered-slide.html
new file mode 100644
index 0000000..9efc38c
--- /dev/null
+++ b/docs/test-layered-slide.html
@@ -0,0 +1,206 @@
+
+
+
+
+DX와 BIM의 개념적 구분과 재정립
+
+
+
+
+
+
+
+

+
+
+
+
DX와 BIM의 개념적 구분과 재정립
+
+
+
+
📋
+
용어 혼용
+
DX와 BIM 개념이
명확히 정립되지 않은 채
혼용되어 사용
+
+
+
+
🏛️
+
정책 사례
+
건설기술진흥 기본계획
BIM 도입 = 디지털화
로 표현
+
+
+
+
📐
+
BIM
+
3D 모델 기반
정보 통합·관리 도구
핵심 인프라 기술
+
+
+
+
🔄
+
DX
+
디지털 기술 기반
산업 패러다임 전환
업무방식·가치 구조 변혁
+
+
+
+
🔗
+
기술 융합
+
GIS + BIM + DT
기술 융합으로만
DX 실현 가능
+
+
+
+
+
DX와 BIM의
관계
+
개념적 구분과 재정립
+
+
+
+
+
상위 개념
+
산업 패러다임 전환
프로세스 혁신
+
+
+
+
핵심 기초 기술
+
건설정보 통합 관리
디지털 협업 인프라
+
+
+
+
+ BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
+
+
+
+
+
diff --git a/docs/test-layered-v2.html b/docs/test-layered-v2.html
new file mode 100644
index 0000000..9c26029
--- /dev/null
+++ b/docs/test-layered-v2.html
@@ -0,0 +1,222 @@
+
+
+
+
+DX와 BIM의 개념적 구분과 재정립
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
DX와 BIM의 개념적 구분과 재정립
+
+
+
+
📋
+
+
용어 혼용
+
DX와 BIM 개념이
정립되지 않은 채
혼용되어 사용
+
+
+
+
+
🏛️
+
+
정책 사례
+
건설기술진흥 기본계획
BIM 도입 = 디지털화
+
+
+
+
+
📐
+
+
BIM
+
3D 모델 기반
정보 통합·관리 도구
+
+
+
+
+
🔄
+
+
DX
+
디지털 기술 기반
산업 패러다임 전환
업무방식·가치 구조 변혁
+
+
+
+
+
🔗
+
+
기술 융합
+
GIS + BIM + DT
기술 융합으로만
DX 실현 가능
+
+
+
+
+
+
+
DX와 BIM의
관계
+
개념적 구분과 재정립
+
+
+
+
+
+
+
→
+
+
상위 개념
+
산업 패러다임 전환
프로세스 혁신
+
+
+
+
+
+
+
+
핵심 기초 기술
+
건설정보 통합 관리
디지털 협업 인프라
+
+
←
+
+
+
+
+
+ BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
+
+
+
+
+
diff --git a/docs/test-relation.html b/docs/test-relation.html
new file mode 100644
index 0000000..90313ba
--- /dev/null
+++ b/docs/test-relation.html
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/docs/test-relation.svg b/docs/test-relation.svg
new file mode 100644
index 0000000..90313ba
--- /dev/null
+++ b/docs/test-relation.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/docs/test-simple.html b/docs/test-simple.html
new file mode 100644
index 0000000..9e169a5
--- /dev/null
+++ b/docs/test-simple.html
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/docs/test-simple.svg b/docs/test-simple.svg
new file mode 100644
index 0000000..9e169a5
--- /dev/null
+++ b/docs/test-simple.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/docs/test-slide-output.html b/docs/test-slide-output.html
new file mode 100644
index 0000000..c914615
--- /dev/null
+++ b/docs/test-slide-output.html
@@ -0,0 +1,402 @@
+
+
+
+
+DX와 BIM의 개념적 구분과 재정립
+
+
+
+
+
+
+
+
DX와 BIM의 개념적 구분과 재정립
+
+
+
+
+
+
+
+
+
+
+
+
제7차 건설기술진흥 기본계획
+
국토교통부, 2023
+
추진방향: 디지털 전환을 통한 스마트 건설 확산
+추진과제: BIM 도입으로 건설산업 디지털화
+
+
+
+
+
+
스마트 건설 활성화 방안
+
국토교통부, 2022
+
추진과제: 건설산업 디지털화
+세부내용: BIM 전면 도입 및 제도 정비, BIM 전문인력 양성
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
BIM
+
디지털 전환 핵심 기술
+
시설물 생애주기 정보를 3D 모델 기반으로 통합·관리하는 인프라 기술
+
건설산업 BIM 기본지침, 국토교통부, 2020
+
+
+
+
+
건설산업
+
종합산업
+
다양한 시설물을 광범위한 기술을 통합·융합하여 만들어내는 종합산업
+
+
+
+
+
+
DX
+
산업 패러다임 변화
+
디지털 기술 기반으로 업무방식과 가치 창출 구조를 전환하는 과정 및 결과
+
IBM, 2011 / Agile Elephant, 2015
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..b12dabc
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1054 @@
+{
+ "name": "design_agent",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "design_agent",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@antv/infographic": "^0.2.16",
+ "infographic-cli": "^0.5.1"
+ }
+ },
+ "node_modules/@antv/event-emitter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@antv/event-emitter/-/event-emitter-0.1.3.tgz",
+ "integrity": "sha512-4ddpsiHN9Pd4UIlWuKVK1C4IiZIdbwQvy9i7DUSI3xNJ89FPUFt8lxDYj8GzzfdllV0NkJTRxnG+FvLk0llidg==",
+ "license": "MIT"
+ },
+ "node_modules/@antv/expr": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@antv/expr/-/expr-1.0.2.tgz",
+ "integrity": "sha512-vrfdmPHkTuiS5voVutKl2l06w1ihBh9A8SFdQPEE+2KMVpkymzGOF1eWpfkbGZ7tiFE15GodVdhhHomD/hdIwg==",
+ "license": "MIT"
+ },
+ "node_modules/@antv/graphlib": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@antv/graphlib/-/graphlib-2.0.4.tgz",
+ "integrity": "sha512-zc/5oQlsdk42Z0ib1mGklwzhJ5vczLFiPa1v7DgJkTbgJ2YxRh9xdarf86zI49sKVJmgbweRpJs7Nu5bIiwv4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/event-emitter": "^0.1.3"
+ }
+ },
+ "node_modules/@antv/hierarchy": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/@antv/hierarchy/-/hierarchy-0.7.1.tgz",
+ "integrity": "sha512-7r22r+HxfcRZp79ZjGmsn97zgC1Iajrv0Mm9DIgx3lPfk+Kme2MG/+EKdZj1iEBsN0rJRzjWVPGL5YrBdVHchw==",
+ "license": "MIT"
+ },
+ "node_modules/@antv/infographic": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/@antv/infographic/-/infographic-0.2.16.tgz",
+ "integrity": "sha512-PmrQ4Wb1MOlr4wxYzBfWhtKKWwXMS6r/JNEjoH6ZYzjchZII9IAw3K1w1S/ZfuUsS7X9rFt7xbY8/tzqHtaJ6A==",
+ "license": "MIT",
+ "workspaces": [
+ "dev",
+ "site"
+ ],
+ "dependencies": {
+ "@antv/hierarchy": "^0.7.0",
+ "@antv/layout": "^2.0.0-beta.0",
+ "culori": "^4.0.2",
+ "d3": "^7.9.0",
+ "eventemitter3": "^5.0.1",
+ "flru": "^1.0.2",
+ "linkedom": "^0.18.12",
+ "lodash-es": "^4.17.21",
+ "measury": "^0.1.5",
+ "postcss": "^8.5.6",
+ "roughjs": "^4.6.6",
+ "round-polygon": "^0.6.7",
+ "tinycolor2": "^1.6.0"
+ }
+ },
+ "node_modules/@antv/layout": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@antv/layout/-/layout-2.0.0.tgz",
+ "integrity": "sha512-aCZ3UdNc40SfT7meFV7QTADY2HCnc0DShVw56CJNTI6oExUIVU736grPuL5Dhb8/JrVaU4Y83QPN/P7KafBzlw==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/event-emitter": "^0.1.3",
+ "@antv/expr": "^1.0.2",
+ "@antv/graphlib": "^2.0.0",
+ "@antv/util": "^3.3.2",
+ "comlink": "^4.4.1",
+ "d3-force": "^3.0.0",
+ "d3-force-3d": "^3.0.5",
+ "d3-octree": "^1.0.2",
+ "d3-quadtree": "^3.0.1",
+ "dagre": "^0.8.5",
+ "ml-matrix": "^6.10.4",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@antv/util": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/@antv/util/-/util-3.3.11.tgz",
+ "integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "gl-matrix": "^3.3.0",
+ "tslib": "^2.3.1"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "license": "ISC"
+ },
+ "node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/comlink": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
+ "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/css-select": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cssom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
+ "license": "MIT"
+ },
+ "node_modules/culori": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/culori/-/culori-4.0.2.tgz",
+ "integrity": "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-binarytree": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
+ "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
+ "license": "MIT"
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force-3d": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
+ "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
+ "license": "MIT",
+ "dependencies": {
+ "d3-binarytree": "1",
+ "d3-dispatch": "1 - 3",
+ "d3-octree": "1",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-octree": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
+ "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
+ "license": "MIT"
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dagre": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
+ "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
+ "license": "MIT",
+ "dependencies": {
+ "graphlib": "^2.1.8",
+ "lodash": "^4.17.15"
+ }
+ },
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/flru": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz",
+ "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/gl-matrix": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
+ "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
+ "license": "MIT"
+ },
+ "node_modules/graphlib": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
+ "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.15"
+ }
+ },
+ "node_modules/hachure-fill": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
+ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
+ "license": "MIT"
+ },
+ "node_modules/html-escaper": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
+ "license": "MIT"
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
+ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "entities": "^7.0.1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/infographic-cli": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/infographic-cli/-/infographic-cli-0.5.1.tgz",
+ "integrity": "sha512-1XmFoghkc99XfM5VgDg0wmC80xbPBlcczFlV5AeYsaOIyul/dAzopSdx/tzs/QxJIobCw3kCFuPpnDseMtVt5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@antv/infographic": "^0.2.11",
+ "chalk": "^5.4.1",
+ "commander": "^12.1.0"
+ },
+ "bin": {
+ "ifgc": "dist/cli.js",
+ "infographic": "dist/cli.js"
+ },
+ "engines": {
+ "node": "^18.19 || >=20.0"
+ }
+ },
+ "node_modules/infographic-cli/node_modules/commander": {
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+ "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-any-array": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-2.0.1.tgz",
+ "integrity": "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==",
+ "license": "MIT"
+ },
+ "node_modules/linkedom": {
+ "version": "0.18.12",
+ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz",
+ "integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==",
+ "license": "ISC",
+ "dependencies": {
+ "css-select": "^5.1.0",
+ "cssom": "^0.5.0",
+ "html-escaper": "^3.0.3",
+ "htmlparser2": "^10.0.0",
+ "uhyphen": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "canvas": ">= 2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
+ "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
+ "license": "MIT"
+ },
+ "node_modules/measury": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/measury/-/measury-0.1.5.tgz",
+ "integrity": "sha512-YS9nhEbFECQzM9V7kyYylASFwpMyajeRvVnpmfRnhZZwbl9AfpBW+C/o7YwmIQLjK8XFzPHt0OGR7GwqoDuWjA==",
+ "license": "MIT"
+ },
+ "node_modules/ml-array-max": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-1.2.4.tgz",
+ "integrity": "sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^2.0.0"
+ }
+ },
+ "node_modules/ml-array-min": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/ml-array-min/-/ml-array-min-1.2.3.tgz",
+ "integrity": "sha512-VcZ5f3VZ1iihtrGvgfh/q0XlMobG6GQ8FsNyQXD3T+IlstDv85g8kfV0xUG1QPRO/t21aukaJowDzMTc7j5V6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^2.0.0"
+ }
+ },
+ "node_modules/ml-array-rescale": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/ml-array-rescale/-/ml-array-rescale-1.3.7.tgz",
+ "integrity": "sha512-48NGChTouvEo9KBctDfHC3udWnQKNKEWN0ziELvY3KG25GR5cA8K8wNVzracsqSW1QEkAXjTNx+ycgAv06/1mQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^2.0.0",
+ "ml-array-max": "^1.2.4",
+ "ml-array-min": "^1.2.3"
+ }
+ },
+ "node_modules/ml-matrix": {
+ "version": "6.12.1",
+ "resolved": "https://registry.npmjs.org/ml-matrix/-/ml-matrix-6.12.1.tgz",
+ "integrity": "sha512-TJ+8eOFdp+INvzR4zAuwBQJznDUfktMtOB6g/hUcGh3rcyjxbz4Te57Pgri8Q9bhSQ7Zys4IYOGhFdnlgeB6Lw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-any-array": "^2.0.1",
+ "ml-array-rescale": "^1.3.7"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/path-data-parser": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
+ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/points-on-curve": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
+ "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==",
+ "license": "MIT"
+ },
+ "node_modules/points-on-path": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
+ "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
+ "license": "MIT",
+ "dependencies": {
+ "path-data-parser": "0.1.0",
+ "points-on-curve": "0.2.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "license": "Unlicense"
+ },
+ "node_modules/roughjs": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
+ "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "hachure-fill": "^0.5.2",
+ "path-data-parser": "^0.1.0",
+ "points-on-curve": "^0.2.0",
+ "points-on-path": "^0.2.1"
+ }
+ },
+ "node_modules/round-polygon": {
+ "version": "0.6.7",
+ "resolved": "https://registry.npmjs.org/round-polygon/-/round-polygon-0.6.7.tgz",
+ "integrity": "sha512-h5rNwOEP6+EHeRuZAGI2/HsNdl6UCKIFvQXzQfv7nTICZ4/sIURe51vOmQJgk/I6KW5LjQ87HJFoASKjaIEySQ==",
+ "license": "MIT"
+ },
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinycolor2": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
+ "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/uhyphen": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
+ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
+ "license": "ISC"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..e8d8c6b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "design_agent",
+ "version": "1.0.0",
+ "description": "콘텐츠를 시각적으로 구조화된 슬라이드 HTML로 변환하는 독립 에이전트.",
+ "main": "index.js",
+ "directories": {
+ "doc": "docs",
+ "test": "tests"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/keimin86/design_agent.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "type": "commonjs",
+ "bugs": {
+ "url": "https://github.com/keimin86/design_agent/issues"
+ },
+ "homepage": "https://github.com/keimin86/design_agent#readme",
+ "dependencies": {
+ "@antv/infographic": "^0.2.16",
+ "infographic-cli": "^0.5.1"
+ }
+}
diff --git a/src/design_director.py b/src/design_director.py
index a536ec4..e6075be 100644
--- a/src/design_director.py
+++ b/src/design_director.py
@@ -81,11 +81,28 @@ def _load_catalog() -> str:
DIRECTOR_PROMPT = """당신은 디자인 팀장이다. 실장이 분석한 꼭지 목록을 받아 레이아웃을 설계한다.
## 역할
+- 실장의 info_structure(정보 구조)와 각 꼭지의 role(flow/reference)을 **반드시 존중**한다
- 각 꼭지에 적합한 블록을 매핑한다
- 전체 공간을 배분하고 겹침을 방지한다
- 각 블록의 글자 수 가이드를 결정한다
- **텍스트는 절대 정리하지 않는다** (텍스트 편집자가 별도로 한다)
+## 정보 구조 기반 배치 (가장 중요한 규칙)
+실장이 각 꼭지에 role을 부여했다. 이 role에 따라 배치 영역이 결정된다:
+- **role: "flow"** (본문 흐름) → 좌측 또는 메인 영역에 배치. 위→아래 순서대로.
+- **role: "reference"** (참조 정보) → 우측 사이드 영역에 독립 배치. 본문 흐름과 분리.
+- **detail_target: true** (상세 내용) → 본문에 넣지 않는다. popup/자세히보기로 분리.
+
+배치 예시:
+- 본문 흐름(flow) 꼭지 3개 + 참조(reference) 꼭지 1개 → 좌측에 flow 3개, 우측에 reference 1개
+- 모든 꼭지가 flow → 단일 컬럼 또는 균등 분할
+- detail_target 꼭지 → 해당 블록에 연결된 별도 영역 (현재 블록 없으면 생략)
+
+## 중복 방지 규칙
+- 같은 내용이 두 개 블록에 나오면 안 된다
+- 예: 용어 정의가 카드에도 있고 비교 블록에도 있으면 → 하나만 선택
+- 블록 타입이 다르더라도 같은 내용이면 중복
+
## {catalog}
## 이미지 처리 규칙
@@ -99,12 +116,13 @@ DIRECTOR_PROMPT = """당신은 디자인 팀장이다. 실장이 분석한 꼭
- 공간에 안 들어가면 → 요약 요청 또는 페이지 분리
## 자세히보기 규칙
-- 너무 구체적/세부적인 내용은 details-block으로 설계
-- 슬라이드 표면: 요약만, 펼치면: 전체 상세
+- detail_target: true인 꼭지는 본문에 넣지 않는다
+- 관련된 블록 근처에 popup/링크로 연결
## 공간 배분 규칙
- CSS grid-template-areas 형식으로 배치
- 영역명: header, left, right, center, main, footer 등
+- flow 꼭지는 좌측/메인, reference 꼭지는 우측/사이드
- 꼭지끼리 겹치지 않도록 설계
- 각 블록에 대략적 크기 감(small/medium/large) 제시
@@ -155,14 +173,18 @@ async def create_layout_concept(
catalog_text = _load_catalog()
- # 꼭지 요약
+ # 꼭지 요약 (role과 detail_target 포함)
topics_summary = []
for t in analysis.get("topics", []):
+ role = t.get("role", "flow")
line = (
f"꼭지 {t['id']}: {t['title']} "
- f"[{t.get('layer', '?')}, 강조:{t.get('emphasis', False)}, "
+ f"[{t.get('layer', '?')}, ROLE:{role}, "
+ f"강조:{t.get('emphasis', False)}, "
f"방향:{t.get('direction', '?')}, 유형:{t.get('content_type', 'text')}]"
)
+ if t.get("detail_target"):
+ line += " → ★자세히보기 대상 (본문에 넣지 마라)"
if t.get("image_info"):
line += f" 이미지:{t['image_info']}"
if t.get("table_info"):
@@ -173,14 +195,21 @@ async def create_layout_concept(
system = DIRECTOR_PROMPT.replace("{catalog}", catalog_text)
+ info_structure = analysis.get("info_structure", "정보 구조 미분석")
+
user_prompt = (
f"## 실장 분석 결과\n"
f"제목: {analysis.get('title', '')}\n"
f"페이지 수: {analysis.get('total_pages', 1)}\n"
+ f"정보 구조: {info_structure}\n\n"
f"꼭지 목록:\n" + "\n".join(topics_summary) +
f"\n\n## 원본 콘텐츠 (분량 참고)\n{content[:2000]}\n\n"
f"## 요청\n"
- f"위 꼭지를 어떤 블록으로, 어디에, 몇 페이지로 배치할지 설계해줘.\n"
+ f"위 꼭지를 어떤 블록으로, 어디에 배치할지 설계해줘.\n"
+ f"반드시 각 꼭지의 ROLE(flow/reference)에 따라 영역을 배정해라.\n"
+ f"flow → 좌측/메인, reference → 우측/사이드.\n"
+ f"detail_target → 본문에 넣지 마라.\n"
+ f"같은 내용이 두 블록에 중복되면 안 된다.\n"
f"텍스트는 채우지 마. 구조만 JSON으로."
)
diff --git a/src/kei_client.py b/src/kei_client.py
index b0c59ab..29ca6bb 100644
--- a/src/kei_client.py
+++ b/src/kei_client.py
@@ -1,7 +1,7 @@
"""DA-12: 1단계 — Kei 실장 (꼭지 추출 + 분석).
-본문에서 핵심 꼭지를 추출하고, 각 꼭지의 레이어/강조/배치 방향을 분석한다.
-이미지/표/상세 콘텐츠도 판단한다.
+1차: Kei API를 통해 Kei persona가 사고하여 꼭지를 추출한다.
+fallback: Kei API 실패 시 Anthropic API 직접 호출.
"""
from __future__ import annotations
@@ -11,103 +11,191 @@ import re
from typing import Any
import anthropic
+import httpx
from src.config import settings
logger = logging.getLogger(__name__)
-CLASSIFICATION_PROMPT = """당신은 콘텐츠를 분석하여 슬라이드 구조를 설계하는 실장이다.
-
-## 역할
-본문에서 핵심 꼭지를 추출하고, 각 꼭지의 성격을 분석하여 슬라이드 구조를 설계한다.
-
-## 꼭지 추출 규칙
-- 본문에서 2~5개의 핵심 꼭지(파트)를 추출한다
-- 1페이지 적정 꼭지 수: 5개
-- 꼭지가 5개를 넘고 중요도가 동등하면 → 2페이지로 분리 (의미 기반 분할)
-- 5개인데 내용이 많으면 → 세부 내용은 "자세히보기" 대상으로 표시
-
-## 각 꼭지 분석 항목
-1. **레이어 수준**: 도입(문제 제기, 배경) / 핵심(핵심 내용, 정의) / 보조(사례, 근거) / 결론(요약, 핵심 메시지)
-2. **강조**: 눈에 띄게 해야 하는 꼭지 표시 (true/false)
-3. **배치 방향**: 세로로 긴 내용(vertical) / 가로로 나열(horizontal) / 유연(flexible)
-4. **콘텐츠 유형**: text(텍스트) / image(이미지) / table(표) / mixed(혼합)
-5. **이미지 정보** (이미지가 있는 경우):
- - 핵심인지 보조인지 (core/supplementary)
- - 텍스트 포함 여부 (도표/차트는 true)
-6. **표 정보** (표가 있는 경우):
- - 대략적 행/열 수
- - 전체 표시 가능한지 판단
-7. **자세히보기 대상**: 너무 구체적/세부적인 내용은 detail_target: true
-
-## 출력 형식 (반드시 JSON만. 설명 없이.)
-```json
-{
- "title": "슬라이드 제목",
- "total_pages": 1,
- "topics": [
- {
- "id": 1,
- "title": "꼭지 제목",
- "summary": "꼭지 내용 요약 (1~2줄)",
- "layer": "intro|core|supporting|conclusion",
- "emphasis": true,
- "direction": "vertical|horizontal|flexible",
- "content_type": "text|image|table|mixed",
- "image_info": {"role": "core|supplementary", "has_text": true},
- "table_info": {"rows": 5, "cols": 3, "fits_page": true},
- "detail_target": false,
- "page": 1
- }
- ]
-}
-```"""
+KEI_PROMPT = (
+ "다음 콘텐츠를 슬라이드로 정리하려고 해.\n\n"
+ "## 1단계: 정보 구조 파악 (꼭지 추출 전에 먼저 수행)\n"
+ "- 이 콘텐츠가 하나의 흐름으로 읽히는가, 아니면 '본문 흐름'과 '참조 정보'로 분리되는 구조인가?\n"
+ "- 독립적으로 참조되는 정보(용어 정의, 부록, 별도 설명)가 있는가?\n"
+ "- 상세히 다뤄야 하지만 본문 흐름을 끊는 내용(비교표, 상세 데이터)이 있는가?\n"
+ "- 정보 구조를 info_structure 필드에 기술해줘.\n\n"
+ "## 2단계: 꼭지 추출\n"
+ "- 1단계에서 파악한 정보 구조를 바탕으로 꼭지를 나눠줘\n"
+ "- 원본의 논리 흐름과 정보를 빠뜨리지 마라\n"
+ "- 원본에 있는 내용을 임의로 제거하거나 다른 의미로 바꾸지 마라\n"
+ "- 슬라이드에 맞게 정리하되, 원본이 말하려는 흐름은 유지\n"
+ "- 각 꼭지의 레이어(도입/핵심/보조/결론), 강조 여부, 배치 방향을 판단해줘\n"
+ "- 참조 정보는 role: 'reference'로, 본문 흐름은 role: 'flow'로 표시\n"
+ "- 본문 흐름을 끊는 상세 내용은 detail_target: true로 표시\n"
+ "- 이미지/표가 있으면 그것도 판단해줘\n"
+ "- 1페이지 적정 꼭지: 5개. 초과 시 2페이지 분리.\n\n"
+ "## 출력 형식 (JSON만)\n"
+ "```json\n"
+ '{"title": "제목", "total_pages": 1, '
+ '"info_structure": "이 콘텐츠의 정보 구조 설명 (본문 흐름 vs 참조 분리 등)", '
+ '"topics": ['
+ '{"id": 1, "title": "꼭지 제목", "summary": "요약", '
+ '"layer": "intro|core|supporting|conclusion", '
+ '"role": "flow|reference", '
+ '"emphasis": true, "direction": "vertical|horizontal|flexible", '
+ '"content_type": "text|image|table|mixed", '
+ '"detail_target": false, "page": 1}]}\n'
+ "```\n\n"
+ "## 콘텐츠:\n"
+)
async def classify_content(content: str) -> dict[str, Any] | None:
- """1단계: 본문에서 꼭지를 추출하고 분석한다.
+ """1단계: Kei API를 통해 꼭지를 추출하고 분석한다.
- Args:
- content: 원본 텍스트 콘텐츠
-
- Returns:
- 분류 결과 JSON. 실패 시 None.
+ 1차: Kei API (persona + RAG + 사고)
+ fallback: Anthropic API 직접 호출
"""
- if not settings.anthropic_api_key:
- logger.warning("ANTHROPIC_API_KEY 미설정. 수동 분류 모드.")
- return None
+ # 1차: Kei API
+ result = await _call_kei_api(content)
+ if result:
+ logger.info(
+ f"[Kei API] 꼭지 추출 완료: {result.get('title', '')}, "
+ f"{len(result.get('topics', []))}개 꼭지"
+ )
+ return result
+
+ # fallback: Anthropic 직접
+ logger.warning("Kei API 실패. Anthropic 직접 호출로 fallback.")
+ result = await _call_anthropic_direct(content)
+ if result:
+ logger.info(
+ f"[Anthropic] 꼭지 추출 완료: {result.get('title', '')}, "
+ f"{len(result.get('topics', []))}개 꼭지"
+ )
+ return result
+
+ return None
+
+
+async def _call_kei_api(content: str) -> dict[str, Any] | None:
+ """Kei API를 통해 꼭지 추출. SSE 스트리밍 응답을 파싱."""
+ kei_url = getattr(settings, "kei_api_url", "http://localhost:8000")
try:
- client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
-
- response = await client.messages.create(
- model="claude-sonnet-4-20250514",
- max_tokens=2048,
- system=CLASSIFICATION_PROMPT,
- messages=[
- {
- "role": "user",
- "content": f"다음 콘텐츠를 분석하여 꼭지를 추출하고 구조를 설계해줘:\n\n{content}",
- }
- ],
- )
-
- result_text = response.content[0].text
- analysis = _parse_json(result_text)
-
- if analysis and "topics" in analysis:
- logger.info(
- f"꼭지 추출 완료: {analysis.get('title', 'untitled')}, "
- f"{len(analysis['topics'])}개 꼭지, "
- f"{analysis.get('total_pages', 1)}페이지"
+ async with httpx.AsyncClient(timeout=None) as client:
+ response = await client.post(
+ f"{kei_url}/api/message",
+ json={
+ "message": KEI_PROMPT + content,
+ "session_id": "design-agent",
+ "mode": "chat",
+ },
+ timeout=None,
)
- return analysis
- else:
- logger.warning(f"분류 JSON 파싱 실패. 응답: {result_text[:200]}")
+
+ if response.status_code != 200:
+ logger.warning(f"Kei API HTTP {response.status_code}")
+ return None
+
+ # SSE 응답에서 토큰 수집
+ full_text = _extract_sse_text(response.text)
+
+ if not full_text:
+ logger.warning("Kei API 응답에서 텍스트 추출 실패")
+ return None
+
+ # JSON 추출
+ result = _parse_json(full_text)
+ if result and "topics" in result:
+ return result
+
+ logger.warning(f"Kei API JSON 파싱 실패. 텍스트: {full_text[:200]}")
return None
except Exception as e:
- logger.warning(f"실장 분류 호출 실패: {e}")
+ logger.warning(f"Kei API 호출 실패: {e}")
+ return None
+
+
+def _extract_sse_text(raw: str) -> str:
+ """SSE 응답에서 토큰 텍스트를 수집한다. CRLF/LF 모두 처리."""
+ tokens = []
+ # CRLF 또는 LF로 이벤트 분리
+ events = re.split(r'\r?\n\r?\n', raw)
+
+ for event in events:
+ if not event.strip():
+ continue
+
+ event_type = ""
+ event_data = ""
+
+ for line in event.split('\n'):
+ line = line.strip('\r')
+ if line.startswith('event:'):
+ event_type = line[6:].strip()
+ elif line.startswith('data:'):
+ event_data = line[5:].strip()
+
+ if not event_data:
+ continue
+
+ if event_type == 'token':
+ try:
+ token = json.loads(event_data)
+ if isinstance(token, str):
+ tokens.append(token)
+ except json.JSONDecodeError:
+ tokens.append(event_data)
+ elif event_type == 'done':
+ break
+
+ return "".join(tokens)
+
+
+async def _call_anthropic_direct(content: str) -> dict[str, Any] | None:
+ """Anthropic API 직접 호출 (Kei API fallback)."""
+ if not settings.anthropic_api_key:
+ return None
+
+ system_prompt = (
+ "당신은 콘텐츠를 분석하여 슬라이드 구조를 설계하는 실장이다.\n\n"
+ "## 핵심 원칙\n"
+ "- 원본의 논리 흐름과 정보를 빠뜨리지 마라\n"
+ "- 원본에 있는 내용을 임의로 제거하거나 다른 의미로 바꾸지 마라\n"
+ "- 슬라이드에 맞게 정리하되, 원본이 말하려는 흐름은 유지\n\n"
+ "## 꼭지 추출 규칙\n"
+ "- 본문에서 2~5개의 핵심 꼭지를 추출한다\n"
+ "- 1페이지 적정 꼭지 수: 5개\n"
+ "- 초과 시 2페이지 분리\n\n"
+ "## 출력 형식 (JSON만. 설명 없이.)\n"
+ '{"title": "제목", "total_pages": 1, "topics": ['
+ '{"id": 1, "title": "꼭지 제목", "summary": "요약", '
+ '"layer": "intro|core|supporting|conclusion", '
+ '"emphasis": true, "direction": "vertical|horizontal|flexible", '
+ '"content_type": "text", "detail_target": false, "page": 1}]}'
+ )
+
+ try:
+ client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
+ response = await client.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=2048,
+ system=system_prompt,
+ messages=[{"role": "user", "content": f"다음 콘텐츠의 꼭지를 추출해줘:\n\n{content}"}],
+ )
+
+ result_text = response.content[0].text
+ result = _parse_json(result_text)
+
+ if result and "topics" in result:
+ return result
+
+ return None
+
+ except Exception as e:
+ logger.warning(f"Anthropic 직접 호출 실패: {e}")
return None
@@ -129,7 +217,7 @@ def _parse_json(text: str) -> dict[str, Any] | None:
def manual_classify(content: str) -> dict[str, Any]:
- """실장 분류 실패 시 기본 구조를 반환하는 fallback."""
+ """분류 실패 시 기본 구조 fallback."""
return {
"title": "슬라이드",
"total_pages": 1,
diff --git a/src/pipeline.py b/src/pipeline.py
index 4007c19..2b8cad3 100644
--- a/src/pipeline.py
+++ b/src/pipeline.py
@@ -10,6 +10,7 @@ from __future__ import annotations
import json
import logging
+import re
from typing import Any, AsyncIterator
import anthropic
diff --git a/templates/catalog.yaml b/templates/catalog.yaml
new file mode 100644
index 0000000..3964452
--- /dev/null
+++ b/templates/catalog.yaml
@@ -0,0 +1,260 @@
+# Design Agent 블록 카탈로그
+# 디자인 팀장이 콘텐츠에 적합한 블록을 선택할 때 참조하는 메뉴판
+#
+# 규칙:
+# - when: 이 블록을 선택해야 하는 상황
+# - not_for: 이 블록을 선택하면 안 되는 상황 (유사 블록과 구분 핵심)
+# - slots.required: 반드시 채워야 하는 슬롯
+# - slots.optional: 있으면 좋지만 없어도 됨
+# - character_limits: 팀장의 글자 수 가이드 참고용 (하드코딩 기준 아님, 판단의 참고치)
+
+version: "1.0"
+
+blocks:
+
+ # ──────────────────────────────────────
+ # 1. 강조 인용 (quote-block)
+ # ──────────────────────────────────────
+ - id: quote-block
+ name: 강조 인용
+ template: blocks/quote-block.html
+ visual: "좌측 컬러 라인(빨간색) + 연한 배경 + 인용 텍스트. 출처 표기 가능."
+ when: >
+ 문제 제기, 핵심 주장, 정의 강조할 때.
+ 원문에서 따온 중요한 문장을 부각시킬 때.
+ 슬라이드 상단에 배치하여 이슈를 먼저 던질 때.
+ not_for: >
+ 일반 설명문 (일반 텍스트는 card-grid이나 본문 영역 사용).
+ 사례 나열 (출처가 있는 사례는 example-card 사용).
+ 결론 요약 (하단 결론은 conclusion-bar 사용).
+ slots:
+ required:
+ - quote_text
+ optional:
+ - source
+ character_limits:
+ quote_text: 150
+ source: 50
+
+ # ──────────────────────────────────────
+ # 2. 카드 그리드 (card-grid)
+ # ──────────────────────────────────────
+ - id: card-grid
+ name: 카드 그리드
+ template: blocks/card-grid.html
+ visual: "2~4열 카드 나란히. 각 카드 상단에 컬러 액센트 라인. 아이콘/제목/카테고리/설명/출처."
+ when: >
+ 용어를 여러 개 정의할 때 (2~4개).
+ 개념/기능/특성을 나란히 비교 없이 나열할 때.
+ 각 항목에 제목+설명+출처가 있을 때.
+ not_for: >
+ A vs B 직접 비교 (comparison 사용).
+ 출처가 있는 정책 사례 인용 (example-card 사용, 향후 추가).
+ 기능 목록이 아이콘 중심일 때 (icon-list 사용, 향후 추가).
+ 순서가 있는 항목 (process 사용).
+ slots:
+ required:
+ - "cards[]"
+ optional: []
+ card_fields:
+ required:
+ - title
+ - description
+ optional:
+ - icon
+ - category
+ - color
+ - source
+ character_limits:
+ title: 20
+ category: 15
+ description: 80
+ source: 40
+
+ # ──────────────────────────────────────
+ # 3. 비교 (comparison)
+ # ──────────────────────────────────────
+ - id: comparison
+ name: 2단 비교
+ template: blocks/comparison.html
+ visual: "좌우 2단 병렬. 중앙 세로 구분선. 좌측은 파란 액센트, 우측은 빨간 액센트."
+ when: >
+ A vs B 직접 비교할 때.
+ 장단점, Before/After, 현재/미래를 대비할 때.
+ 두 개념의 차이를 명확히 보여줄 때.
+ not_for: >
+ 3개 이상 항목 비교 (comparison-table 사용).
+ 비교가 아닌 단순 나열 (card-grid 사용).
+ 수치 비교 (big-number 사용, 향후 추가).
+ slots:
+ required:
+ - left_title
+ - left_content
+ - right_title
+ - right_content
+ optional:
+ - left_subtitle
+ - right_subtitle
+ character_limits:
+ left_title: 15
+ right_title: 15
+ left_content: 200
+ right_content: 200
+ left_subtitle: 30
+ right_subtitle: 30
+
+ # ──────────────────────────────────────
+ # 4. 관계도 (relationship)
+ # ──────────────────────────────────────
+ - id: relationship
+ name: 관계도 (벤 다이어그램)
+ template: blocks/relationship.html
+ visual: "큰 원(상위 개념) 안에 작은 원 2~3개(하위 요소). 하단에 관계 설명."
+ when: >
+ 상위-하위 관계, 포함 관계를 시각화할 때.
+ 기술 융합/통합 구조를 보여줄 때 (예: DX = GIS + BIM + DT).
+ 2~3개 요소가 하나의 큰 개념에 속함을 보여줄 때.
+ not_for: >
+ 순서가 있는 흐름 (process 사용).
+ 대등한 비교 (comparison 사용).
+ 4개 이상 요소 나열 (card-grid 사용).
+ slots:
+ required:
+ - center_label
+ - "items[]"
+ optional:
+ - center_sub
+ - description
+ item_fields:
+ required:
+ - label
+ optional:
+ - color
+ character_limits:
+ center_label: 15
+ center_sub: 20
+ item_label: 10
+ description: 100
+
+ # ──────────────────────────────────────
+ # 5. 프로세스 (process)
+ # ──────────────────────────────────────
+ - id: process
+ name: 단계 흐름
+ template: blocks/process.html
+ visual: "가로 방향 단계. 각 단계는 번호 원형 + 제목 + 설명. 단계 사이에 → 화살표."
+ when: >
+ 순서가 있는 절차를 보여줄 때 (1→2→3→4).
+ 워크플로우, 실행 단계, 파이프라인.
+ 시간 순서가 아닌 논리적 순서.
+ not_for: >
+ 시간 기반 순서 (timeline 사용, 향후 추가).
+ 순서 없는 항목 나열 (card-grid 사용).
+ 2개 비교 (comparison 사용).
+ slots:
+ required:
+ - "steps[]"
+ optional: []
+ step_fields:
+ required:
+ - title
+ optional:
+ - number
+ - description
+ character_limits:
+ title: 15
+ description: 60
+
+ # ──────────────────────────────────────
+ # 6. 결론 바 (conclusion-bar)
+ # ──────────────────────────────────────
+ - id: conclusion-bar
+ name: 결론 바
+ template: blocks/conclusion-bar.html
+ visual: "전체 너비 진한 배경(primary색). 중앙 정렬 텍스트. 라벨 + 핵심 한 줄."
+ when: >
+ 슬라이드 하단에 핵심 메시지를 한 줄로 마무리할 때.
+ 전체 내용의 결론, 요약, 시사점을 강조할 때.
+ 항상 슬라이드 맨 아래에 배치.
+ not_for: >
+ 본문 중간의 강조 (quote-block 사용).
+ 여러 줄의 요약 (quote-block 또는 card-grid 사용).
+ slots:
+ required:
+ - conclusion_text
+ optional:
+ - label
+ character_limits:
+ conclusion_text: 80
+ label: 10
+
+ # ──────────────────────────────────────
+ # 7. 비교 테이블 (comparison-table)
+ # ──────────────────────────────────────
+ - id: comparison-table
+ name: 비교 테이블
+ template: blocks/comparison-table.html
+ visual: "표 형태. 진한 배경 헤더 행. 짝수 행 배경 교차. 첫 열 굵은 글씨."
+ when: >
+ 3개 이상 항목을 여러 기준으로 비교할 때.
+ 다차원 비교, 기능 매트릭스, 스펙 비교.
+ 행/열이 5개 이상인 구조화된 데이터.
+ not_for: >
+ 2개 항목 비교 (comparison 사용).
+ 정의/설명 나열 (card-grid 사용).
+ 순서가 있는 데이터 (process 또는 timeline 사용).
+ slots:
+ required:
+ - "headers[]"
+ - "rows[][]"
+ optional: []
+ character_limits:
+ header: 15
+ cell: 40
+
+# ──────────────────────────────────────
+# 레이아웃 옵션
+# ──────────────────────────────────────
+layouts:
+
+ - id: "65-35"
+ name: "6.5:3.5 좌우 분할"
+ grid_columns: "6.5fr 3.5fr"
+ when: "좌측 메인 콘텐츠(인용+사례+이미지) + 우측 보조(정의/용어)"
+
+ - id: "50-50"
+ name: "5:5 균등 분할"
+ grid_columns: "1fr 1fr"
+ when: "대등한 비교, 병렬 콘텐츠"
+
+ - id: "single"
+ name: "단일 컬럼"
+ grid_columns: "1fr"
+ when: "프로세스 흐름, 타임라인, 단순 구조, 블록이 1~2개일 때"
+
+ - id: "35-65"
+ name: "3.5:6.5 좌우 분할"
+ grid_columns: "3.5fr 6.5fr"
+ when: "좌측 요약/네비게이션 + 우측 메인 콘텐츠"
+
+ - id: "40-60"
+ name: "4:6 좌우 분할"
+ grid_columns: "4fr 6fr"
+ when: "좌측 설명/정의 + 우측 시각화(관계도/이미지)"
+
+ - id: "60-40"
+ name: "6:4 좌우 분할"
+ grid_columns: "6fr 4fr"
+ when: "좌측 메인 텍스트 + 우측 보조 정보"
+
+# ──────────────────────────────────────
+# 향후 추가 예정 블록 (Figma 추출 후)
+# ──────────────────────────────────────
+# - example-card: 출처+불릿 사례 카드
+# - image-block: 이미지 블록 (full/side/thumb)
+# - image-gallery: 이미지 갤러리 (2col/3col/2x2)
+# - timeline: 타임라인 (vertical/horizontal)
+# - big-number: 핵심 지표 (큰 숫자)
+# - icon-list: 아이콘 리스트
+# - section-title: 섹션 타이틀 (영문+한글)
+# - details-block: 자세히보기 (/)