Kei API 연동 복구 + 실장 정보구조 분석 + 팀장 role 기반 배치

1단계 (실장):
  - Kei API 연동 복구 (타임아웃 무제한, Kei persona 사고)
  - 정보 구조 파악 단계 추가 (본문 흐름 vs 참조 분리)
  - 각 꼭지에 role(flow/reference) 부여
  - fallback: Anthropic 직접 호출 (info_structure + role 포함)

2단계 (팀장):
  - info_structure + role 기반 배치 규칙 추가
  - flow → 좌측/메인, reference → 우측/사이드
  - detail_target → 본문 제외
  - 중복 방지 규칙

파이프라인:
  - pipeline.py import re 추가

Figma 관련 (다른 Claude Code 작업분):
  - catalog.yaml, figma-screenshots, figma-analysis, 테스트 HTML

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 11:33:17 +09:00
parent 33bd3a56c6
commit 7b034b04b6
24 changed files with 2400 additions and 90 deletions

View File

@@ -7,7 +7,7 @@
"npx", "npx",
"-y", "-y",
"figma-developer-mcp", "figma-developer-mcp",
"--figma-api-key=figd_R6ASvFG2IHcHs35_XFPJh0sTkvp4RxWyEhMhT9vv", "--figma-api-key=figd_s23TfSDL0hS97DIialy0R2P6QsoZQHfuGx1l_t-k",
"--stdio" "--stdio"
] ]
} }

BIN
docs/bg-texture-only.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

View File

@@ -0,0 +1 @@
{"status":429,"err":"Rate limit exceeded"}

View File

@@ -0,0 +1 @@
{"status":429,"err":"Rate limit exceeded"}

View File

@@ -0,0 +1 @@
{"status":429,"err":"Rate limit exceeded"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

BIN
docs/test-bg-layer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,206 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>DX와 BIM의 개념적 구분과 재정립</title>
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Pretendard Variable', sans-serif; }
.slide {
width: 1280px;
height: 720px;
position: relative;
overflow: hidden;
}
/* Layer 1: 배경 이미지 */
.bg-layer {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
}
.bg-layer img {
width: 100%; height: 100%;
object-fit: cover;
}
/* Layer 2: 슬라이드 제목 */
.title-layer {
position: absolute;
top: 15px; left: 30px;
z-index: 10;
font-size: 1.6rem;
font-weight: 900;
color: #1e293b;
border-bottom: 3px solid #2563eb;
padding-bottom: 6px;
}
/* Layer 3: 상단 5개 원 위 아이콘 + 텍스트 */
.circle-labels {
position: absolute;
z-index: 10;
text-align: center;
}
.circle-labels .icon {
font-size: 1.8rem;
margin-bottom: 2px;
}
.circle-labels .label {
font-size: 0.95rem;
font-weight: 800;
color: #1e293b;
line-height: 1.3;
}
.circle-labels .desc {
font-size: 0.7rem;
color: #64748b;
line-height: 1.4;
margin-top: 2px;
}
.circle-labels .highlight {
color: #2563eb;
font-weight: 700;
}
/* 각 원 위치 (배경 이미지의 원 위치에 맞춤) */
.circle-1 { top: 175px; left: 52px; width: 120px; }
.circle-2 { top: 95px; left: 245px; width: 120px; }
.circle-3 { top: 50px; left: 460px; width: 120px; }
.circle-4 { top: 95px; left: 680px; width: 120px; }
.circle-5 { top: 175px; left: 870px; width: 140px; }
/* Layer 4: 중앙 큰 원 텍스트 */
.center-label {
position: absolute;
top: 410px; left: 440px;
width: 200px;
z-index: 10;
text-align: center;
}
.center-label .main {
font-size: 1.3rem;
font-weight: 900;
color: #1e293b;
line-height: 1.4;
}
.center-label .sub {
font-size: 0.75rem;
color: #64748b;
margin-top: 4px;
}
/* Layer 5: 좌우 하단 박스 텍스트 */
.bottom-box {
position: absolute;
bottom: 35px;
z-index: 10;
padding: 8px 20px;
text-align: center;
}
.bottom-left {
left: 30px;
width: 260px;
}
.bottom-right {
right: 30px;
width: 260px;
}
.bottom-box .box-title {
font-size: 0.7rem;
color: #64748b;
margin-bottom: 2px;
}
.bottom-box .box-content {
font-size: 0.85rem;
font-weight: 700;
color: #1e293b;
line-height: 1.4;
}
/* Layer 6: 하단 결론 바 */
.conclusion-bar {
position: absolute;
bottom: 0; left: 0; right: 0;
z-index: 10;
background: #1e293b;
color: white;
text-align: center;
padding: 10px 40px;
font-size: 1rem;
font-weight: 700;
letter-spacing: -0.3px;
}
</style>
</head>
<body>
<div class="slide">
<!-- Layer 1: AI 생성 배경 이미지 -->
<div class="bg-layer">
<img src="test-bg-layer.png" alt="background">
</div>
<!-- Layer 2: 슬라이드 제목 -->
<div class="title-layer">DX와 BIM의 개념적 구분과 재정립</div>
<!-- Layer 3: 상단 5개 원 위 텍스트 -->
<div class="circle-labels circle-1">
<div class="icon">📋</div>
<div class="label">용어 혼용</div>
<div class="desc">DX와 BIM 개념이<br>명확히 <span class="highlight">정립되지 않은 채</span><br>혼용되어 사용</div>
</div>
<div class="circle-labels circle-2">
<div class="icon">🏛️</div>
<div class="label">정책 사례</div>
<div class="desc">건설기술진흥 기본계획<br><span class="highlight">BIM 도입 = 디지털화</span><br>로 표현</div>
</div>
<div class="circle-labels circle-3">
<div class="icon">📐</div>
<div class="label">BIM</div>
<div class="desc">3D 모델 기반<br><span class="highlight">정보 통합·관리</span> 도구<br>핵심 인프라 기술</div>
</div>
<div class="circle-labels circle-4">
<div class="icon">🔄</div>
<div class="label">DX</div>
<div class="desc">디지털 기술 기반<br><span class="highlight">산업 패러다임 전환</span><br>업무방식·가치 구조 변혁</div>
</div>
<div class="circle-labels circle-5">
<div class="icon">🔗</div>
<div class="label">기술 융합</div>
<div class="desc"><span class="highlight">GIS + BIM + DT</span><br>기술 융합으로만<br>DX 실현 가능</div>
</div>
<!-- Layer 4: 중앙 큰 원 텍스트 -->
<div class="center-label">
<div class="main">DX와 BIM의<br>관계</div>
<div class="sub">개념적 구분과 재정립</div>
</div>
<!-- Layer 5: 좌우 하단 박스 -->
<div class="bottom-box bottom-left">
<div class="box-title">상위 개념</div>
<div class="box-content">산업 패러다임 전환<br>프로세스 혁신</div>
</div>
<div class="bottom-box bottom-right">
<div class="box-title">핵심 기초 기술</div>
<div class="box-content">건설정보 통합 관리<br>디지털 협업 인프라</div>
</div>
<!-- Layer 6: 결론 바 -->
<div class="conclusion-bar">
BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
</div>
</div>
</body>
</html>

222
docs/test-layered-v2.html Normal file
View File

@@ -0,0 +1,222 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>DX와 BIM의 개념적 구분과 재정립</title>
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Pretendard Variable', sans-serif; background: #f0f4f8; display: flex; justify-content: center; padding: 20px; }
.slide {
width: 1280px;
height: 720px;
position: relative;
overflow: hidden;
}
/* ═══ Layer 1: 배경 텍스처 ═══ */
.bg { position: absolute; inset: 0; z-index: 1; }
.bg img { width: 100%; height: 100%; object-fit: cover; }
/* ═══ Layer 2: 연결선 (SVG) ═══ */
.lines-layer {
position: absolute; inset: 0; z-index: 2;
}
/* ═══ Layer 3: 원 (각각 개별) ═══ */
.node {
position: absolute; z-index: 5;
display: flex; flex-direction: column; align-items: center;
}
.node-circle {
width: 90px; height: 90px;
border-radius: 50%;
background: white;
border: 3px solid #d0dce8;
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
display: flex; align-items: center; justify-content: center;
font-size: 2rem;
}
.node-label {
margin-top: 8px; text-align: center;
}
.node-label .title {
font-size: 1rem; font-weight: 800; color: #1e293b;
}
.node-label .desc {
font-size: 0.72rem; color: #64748b; line-height: 1.5; margin-top: 3px;
}
.node-label .highlight {
color: #2563eb; font-weight: 700;
}
/* 중앙 큰 원 */
.center-node {
position: absolute; z-index: 5;
left: 530px; top: 390px;
display: flex; flex-direction: column; align-items: center;
}
.center-circle {
width: 160px; height: 160px;
border-radius: 50%;
background: linear-gradient(180deg, #ffffff 0%, #e8f4fd 100%);
border: 4px solid #2563eb;
box-shadow: 0 0 40px rgba(37, 99, 235, 0.3), 0 0 80px rgba(37, 99, 235, 0.1);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
}
.center-circle .main-text {
font-size: 1.2rem; font-weight: 900; color: #1e293b; text-align: center; line-height: 1.4;
}
.center-circle .sub-text {
font-size: 0.7rem; color: #64748b; margin-top: 4px;
}
/* ═══ Layer 4: 제목 ═══ */
.slide-title {
position: absolute; top: 18px; left: 30px; z-index: 10;
font-size: 1.5rem; font-weight: 900; color: #1e293b;
border-bottom: 3px solid #2563eb; padding-bottom: 6px;
}
/* ═══ Layer 5: 하단 좌우 박스 ═══ */
.bottom-box {
position: absolute; z-index: 10; bottom: 45px;
background: rgba(255,255,255,0.85);
border: 1px solid #d0dce8;
border-radius: 25px;
padding: 10px 24px;
text-align: center;
}
.bottom-left { left: 40px; }
.bottom-right { right: 40px; }
.bottom-box .box-label {
font-size: 0.65rem; color: #94a3b8; font-weight: 500;
}
.bottom-box .box-text {
font-size: 0.9rem; font-weight: 700; color: #1e293b; line-height: 1.5;
}
.bottom-box .arrow {
font-size: 1.2rem; color: #2563eb; font-weight: 900;
}
.bottom-left-wrap, .bottom-right-wrap {
display: flex; align-items: center; gap: 12px;
}
/* ═══ Layer 6: 결론 바 ═══ */
.conclusion {
position: absolute; bottom: 0; left: 0; right: 0; z-index: 10;
background: #1e293b;
color: white; text-align: center;
padding: 10px 40px;
font-size: 0.95rem; font-weight: 700;
}
/* ═══ 5개 노드 위치 ═══ */
.node-1 { left: 50px; top: 170px; width: 130px; }
.node-2 { left: 250px; top: 80px; width: 140px; }
.node-3 { left: 510px; top: 40px; width: 130px; }
.node-4 { left: 770px; top: 80px; width: 150px; }
.node-5 { left: 1000px; top: 170px; width: 140px; }
</style>
</head>
<body>
<div class="slide">
<!-- Layer 1: 배경 텍스처만 -->
<div class="bg">
<img src="bg-texture-only.png" alt="">
</div>
<!-- Layer 2: 연결선 SVG -->
<svg class="lines-layer" viewBox="0 0 1280 720" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 5개 원에서 중앙 원으로 곡선 -->
<path d="M 115,260 Q 300,400 610,470" stroke="#b0c4de" stroke-width="1.5" fill="none" opacity="0.5"/>
<path d="M 320,170 Q 420,350 610,470" stroke="#b0c4de" stroke-width="1.5" fill="none" opacity="0.5"/>
<path d="M 575,130 Q 590,300 610,470" stroke="#b0c4de" stroke-width="1.5" fill="none" opacity="0.5"/>
<path d="M 845,170 Q 750,350 610,470" stroke="#b0c4de" stroke-width="1.5" fill="none" opacity="0.5"/>
<path d="M 1070,260 Q 850,400 610,470" stroke="#b0c4de" stroke-width="1.5" fill="none" opacity="0.5"/>
</svg>
<!-- Layer 3: 슬라이드 제목 -->
<div class="slide-title">DX와 BIM의 개념적 구분과 재정립</div>
<!-- Layer 4: 상단 5개 노드 (각각 원 + 아이콘 + 텍스트) -->
<div class="node node-1">
<div class="node-circle">📋</div>
<div class="node-label">
<div class="title">용어 혼용</div>
<div class="desc">DX와 BIM 개념이<br><span class="highlight">정립되지 않은 채</span><br>혼용되어 사용</div>
</div>
</div>
<div class="node node-2">
<div class="node-circle">🏛️</div>
<div class="node-label">
<div class="title">정책 사례</div>
<div class="desc">건설기술진흥 기본계획<br><span class="highlight">BIM 도입 = 디지털화</span></div>
</div>
</div>
<div class="node node-3">
<div class="node-circle">📐</div>
<div class="node-label">
<div class="title">BIM</div>
<div class="desc">3D 모델 기반<br><span class="highlight">정보 통합·관리</span> 도구</div>
</div>
</div>
<div class="node node-4">
<div class="node-circle">🔄</div>
<div class="node-label">
<div class="title">DX</div>
<div class="desc">디지털 기술 기반<br><span class="highlight">산업 패러다임 전환</span><br>업무방식·가치 구조 변혁</div>
</div>
</div>
<div class="node node-5">
<div class="node-circle">🔗</div>
<div class="node-label">
<div class="title">기술 융합</div>
<div class="desc"><span class="highlight">GIS + BIM + DT</span><br>기술 융합으로만<br>DX 실현 가능</div>
</div>
</div>
<!-- Layer 5: 중앙 큰 원 -->
<div class="center-node">
<div class="center-circle">
<div class="main-text">DX와 BIM의<br>관계</div>
<div class="sub-text">개념적 구분과 재정립</div>
</div>
</div>
<!-- Layer 6: 하단 좌우 박스 -->
<div class="bottom-box bottom-left">
<div class="bottom-left-wrap">
<span class="arrow"></span>
<div>
<div class="box-label">상위 개념</div>
<div class="box-text">산업 패러다임 전환<br>프로세스 혁신</div>
</div>
</div>
</div>
<div class="bottom-box bottom-right">
<div class="bottom-right-wrap">
<div>
<div class="box-label">핵심 기초 기술</div>
<div class="box-text">건설정보 통합 관리<br>디지털 협업 인프라</div>
</div>
<span class="arrow"></span>
</div>
</div>
<!-- Layer 7: 결론 바 -->
<div class="conclusion">
BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
</div>
</div>
</body>
</html>

4
docs/test-relation.html Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Regular/result.css" type="text/css"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Bold/result.css" type="text/css"?>
<svg height="445" width="434.5" style="" font-family="Alibaba PuHuiTi" xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 434.5 445"><defs /><defs><radialGradient id="#1783ff-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#b0d5ff" /><stop offset="100%" stop-color="#1783ff" /></radialGradient><linearGradient id="#ff6b6b-badge" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ff6b6b" /><stop offset="100%" stop-color="#ee5a52" /></linearGradient><radialGradient id="#00c9c9-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#63ffff" /><stop offset="100%" stop-color="#00c9c9" /></radialGradient><radialGradient id="#f0884d-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#fce6da" /><stop offset="100%" stop-color="#f0884d" /></radialGradient><radialGradient id="#d580ff-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#ffffff" /><stop offset="100%" stop-color="#d580ff" /></radialGradient></defs><g id="infographic-container"><g><g><g transform="translate(310.5, 150)"><ellipse x="0" y="0" width="80" height="80" fill="url(##1783ff-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">건설산업 DX</span></foreignObject></g></g><g transform="translate(160.5, 300)"><ellipse x="0" y="0" width="80" height="80" fill="url(##00c9c9-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">GIS</span></foreignObject></g></g><g transform="translate(10.5, 150.00000000000003)"><ellipse x="0" y="0" width="80" height="80" fill="url(##f0884d-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">BIM</span></foreignObject></g></g><g transform="translate(160.49999999999997, 0)"><ellipse x="0" y="0" width="80" height="80" fill="url(##d580ff-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Digital Twin</span></foreignObject></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

4
docs/test-relation.svg Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Regular/result.css" type="text/css"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Bold/result.css" type="text/css"?>
<svg height="445" width="434.5" style="" font-family="Alibaba PuHuiTi" xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 434.5 445"><defs /><defs><radialGradient id="#1783ff-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#b0d5ff" /><stop offset="100%" stop-color="#1783ff" /></radialGradient><linearGradient id="#ff6b6b-badge" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ff6b6b" /><stop offset="100%" stop-color="#ee5a52" /></linearGradient><radialGradient id="#00c9c9-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#63ffff" /><stop offset="100%" stop-color="#00c9c9" /></radialGradient><radialGradient id="#f0884d-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#fce6da" /><stop offset="100%" stop-color="#f0884d" /></radialGradient><radialGradient id="#d580ff-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#ffffff" /><stop offset="100%" stop-color="#d580ff" /></radialGradient></defs><g id="infographic-container"><g><g><g transform="translate(310.5, 150)"><ellipse x="0" y="0" width="80" height="80" fill="url(##1783ff-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">건설산업 DX</span></foreignObject></g></g><g transform="translate(160.5, 300)"><ellipse x="0" y="0" width="80" height="80" fill="url(##00c9c9-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">GIS</span></foreignObject></g></g><g transform="translate(10.5, 150.00000000000003)"><ellipse x="0" y="0" width="80" height="80" fill="url(##f0884d-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">BIM</span></foreignObject></g></g><g transform="translate(160.49999999999997, 0)"><ellipse x="0" y="0" width="80" height="80" fill="url(##d580ff-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Digital Twin</span></foreignObject></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

4
docs/test-simple.html Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Regular/result.css" type="text/css"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Bold/result.css" type="text/css"?>
<svg height="210" width="460" style="" font-family="Alibaba PuHuiTi" xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 460 210"><defs /><g id="infographic-container"><g><g><g><g><g><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Start</span></foreignObject></g><g transform="translate(0, 40)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Step 1</span></foreignObject></g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#1783ff" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">01</text></g></g></g></g><g transform="translate(140, 0)"><g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#00c9c9" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">02</text></g></g><g transform="translate(0, 110)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Step 2</span></foreignObject></g><g transform="translate(0, 130)"><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Progress</span></foreignObject></g></g></g><g transform="translate(280, 0)"><g><g><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Complete</span></foreignObject></g><g transform="translate(0, 40)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Step 3</span></foreignObject></g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#f0884d" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">03</text></g></g></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

4
docs/test-simple.svg Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Regular/result.css" type="text/css"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Bold/result.css" type="text/css"?>
<svg height="210" width="460" style="" font-family="Alibaba PuHuiTi" xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 460 210"><defs /><g id="infographic-container"><g><g><g><g><g><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Start</span></foreignObject></g><g transform="translate(0, 40)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Step 1</span></foreignObject></g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#1783ff" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">01</text></g></g></g></g><g transform="translate(140, 0)"><g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#00c9c9" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">02</text></g></g><g transform="translate(0, 110)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Step 2</span></foreignObject></g><g transform="translate(0, 130)"><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Progress</span></foreignObject></g></g></g><g transform="translate(280, 0)"><g><g><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Complete</span></foreignObject></g><g transform="translate(0, 40)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Step 3</span></foreignObject></g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#f0884d" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">03</text></g></g></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

402
docs/test-slide-output.html Normal file
View File

@@ -0,0 +1,402 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>DX와 BIM의 개념적 구분과 재정립</title>
<style>
/* Design Agent — 디자인 토큰 */
/* CLAUDE.md에 정의된 디자인 원칙을 CSS 변수로 구현 */
:root {
/* 색상 */
--color-primary: #1e293b;
--color-accent: #2563eb;
--color-neutral: #64748b;
--color-bg: #ffffff;
--color-bg-subtle: #f8fafc;
--color-border: #e2e8f0;
--color-danger: #dc2626;
--color-success: #16a34a;
--color-text: #1e293b;
--color-text-secondary: #64748b;
--color-text-light: #94a3b8;
/* 폰트 크기 */
--font-title: 2rem;
--font-subtitle: 1.25rem;
--font-body: 0.95rem;
--font-caption: 0.8rem;
--font-small: 0.7rem;
/* 폰트 두께 */
--weight-normal: 400;
--weight-medium: 500;
--weight-bold: 700;
--weight-black: 900;
/* 여백 */
--spacing-page: 40px;
--spacing-block: 20px;
--spacing-inner: 16px;
--spacing-small: 8px;
/* 기타 */
--radius: 6px;
--border-width: 1px;
--accent-border: 3px;
--line-height-ko: 1.7;
}
/* Design Agent — 기본 슬라이드 스타일 */
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 슬라이드 컨테이너: 16:9 고정 비율 */
.slide {
width: 1280px;
height: 720px;
aspect-ratio: 16 / 9;
overflow: hidden;
background: var(--color-bg);
font-family: 'Pretendard Variable', 'Pretendard', 'Noto Sans KR', sans-serif;
color: var(--color-text);
font-size: var(--font-body);
line-height: var(--line-height-ko);
word-break: keep-all;
padding: var(--spacing-page);
display: grid;
gap: var(--spacing-block);
}
/* 슬라이드 제목 */
.slide-title {
font-size: var(--font-title);
font-weight: var(--weight-black);
color: var(--color-primary);
border-bottom: var(--accent-border) solid var(--color-accent);
padding-bottom: var(--spacing-small);
}
/* 섹션 제목 */
.section-title {
font-size: var(--font-subtitle);
font-weight: var(--weight-bold);
color: var(--color-primary);
margin-bottom: var(--spacing-small);
}
/* 본문 */
.body-text {
font-size: var(--font-body);
color: var(--color-text);
line-height: var(--line-height-ko);
}
/* 캡션/출처 */
.caption {
font-size: var(--font-caption);
color: var(--color-text-light);
font-style: italic;
}
/* 강조 텍스트 */
.highlight {
color: var(--color-accent);
font-weight: var(--weight-bold);
}
/* 경고/문제 강조 */
.danger {
color: var(--color-danger);
font-weight: var(--weight-bold);
}
</style>
<style>
.slide-1 {
grid-template-areas: 'header header' 'left right' 'footer footer';
grid-template-columns: 6.5fr 3.5fr;
grid-template-rows: auto 1fr auto;
}
.slide-1 .area-header {
grid-area: header;
}
.slide-1 .area-left {
grid-area: left;
}
.slide-1 .area-right {
grid-area: right;
}
.slide-1 .area-footer {
grid-area: footer;
}
/* 다중 페이지: 페이지 간 간격 */
.slide + .slide {
margin-top: 40px;
}
/* 인쇄 시 페이지 분리 */
@media print {
.slide {
page-break-after: always;
}
.slide + .slide {
margin-top: 0;
}
}
</style>
</head>
<body>
<div class="slide slide-1">
<div class="slide-title" style="grid-area: header;">DX와 BIM의 개념적 구분과 재정립</div>
<div class="area-header">
<!-- 강조 인용 블록: 문제 제기, 핵심 메시지 -->
<div class="block-quote">
<div class="quote-text">건설산업의 디지털 전환 논의에서 DX와 BIM이 개념적으로 명확히 정립되지 않은 채 혼용되어 사용되고 있으며, BIM 기술의 도입을 DX의 완성으로 오인하거나 DX를 BIM 기술 도입 수준으로 한정하는 인식이 확산되고 있다.</div>
</div>
<style>
.block-quote {
background: var(--color-bg-subtle);
border-left: var(--accent-border) solid var(--color-danger);
padding: var(--spacing-inner) var(--spacing-block);
border-radius: 0 var(--radius) var(--radius) 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.quote-text {
font-size: var(--font-body);
color: var(--color-text);
line-height: var(--line-height-ko);
font-weight: var(--weight-medium);
}
.quote-source {
font-size: var(--font-caption);
color: var(--color-text-light);
font-style: italic;
margin-top: var(--spacing-small);
}
</style>
</div>
<div class="area-left">
<!-- 카드 그리드 블록: 2~4열 카드 배열 -->
<div class="block-card-grid" style="--card-count: 2">
<div class="card" style="border-top-color: None">
<div class="card-title">제7차 건설기술진흥 기본계획</div>
<span class="card-category">국토교통부, 2023</span>
<div class="card-description">추진방향: 디지털 전환을 통한 스마트 건설 확산
추진과제: BIM 도입으로 건설산업 디지털화</div>
</div>
<div class="card" style="border-top-color: None">
<div class="card-title">스마트 건설 활성화 방안</div>
<span class="card-category">국토교통부, 2022</span>
<div class="card-description">추진과제: 건설산업 디지털화
세부내용: BIM 전면 도입 및 제도 정비, BIM 전문인력 양성</div>
</div>
</div>
<style>
.block-card-grid {
display: grid;
grid-template-columns: repeat(var(--card-count, 3), 1fr);
gap: var(--spacing-inner);
height: 100%;
}
.card {
background: var(--color-bg);
border: var(--border-width) solid var(--color-border);
border-top: var(--accent-border) solid var(--color-accent);
border-radius: var(--radius);
padding: var(--spacing-inner);
display: flex;
flex-direction: column;
}
.card-icon {
font-size: 1.5rem;
margin-bottom: var(--spacing-small);
}
.card-title {
font-size: var(--font-subtitle);
font-weight: var(--weight-bold);
color: var(--color-primary);
margin-bottom: 4px;
}
.card-category {
font-size: var(--font-small);
font-weight: var(--weight-medium);
color: var(--color-accent);
background: #dbeafe;
padding: 2px 8px;
border-radius: 12px;
display: inline-block;
margin-bottom: var(--spacing-small);
width: fit-content;
}
.card-description {
font-size: var(--font-body);
color: var(--color-text);
line-height: var(--line-height-ko);
flex: 1;
}
.card-source {
font-size: var(--font-small);
color: var(--color-text-light);
font-style: italic;
margin-top: var(--spacing-small);
border-top: var(--border-width) solid var(--color-border);
padding-top: var(--spacing-small);
}
</style>
</div>
<div class="area-right">
<!-- 카드 그리드 블록: 2~4열 카드 배열 -->
<div class="block-card-grid" style="--card-count: 3">
<div class="card" style="border-top-color: None">
<div class="card-title">BIM</div>
<span class="card-category">디지털 전환 핵심 기술</span>
<div class="card-description">시설물 생애주기 정보를 3D 모델 기반으로 통합·관리하는 인프라 기술</div>
<div class="card-source">건설산업 BIM 기본지침, 국토교통부, 2020</div>
</div>
<div class="card" style="border-top-color: None">
<div class="card-title">건설산업</div>
<span class="card-category">종합산업</span>
<div class="card-description">다양한 시설물을 광범위한 기술을 통합·융합하여 만들어내는 종합산업</div>
</div>
<div class="card" style="border-top-color: None">
<div class="card-title">DX</div>
<span class="card-category">산업 패러다임 변화</span>
<div class="card-description">디지털 기술 기반으로 업무방식과 가치 창출 구조를 전환하는 과정 및 결과</div>
<div class="card-source">IBM, 2011 / Agile Elephant, 2015</div>
</div>
</div>
<style>
.block-card-grid {
display: grid;
grid-template-columns: repeat(var(--card-count, 3), 1fr);
gap: var(--spacing-inner);
height: 100%;
}
.card {
background: var(--color-bg);
border: var(--border-width) solid var(--color-border);
border-top: var(--accent-border) solid var(--color-accent);
border-radius: var(--radius);
padding: var(--spacing-inner);
display: flex;
flex-direction: column;
}
.card-icon {
font-size: 1.5rem;
margin-bottom: var(--spacing-small);
}
.card-title {
font-size: var(--font-subtitle);
font-weight: var(--weight-bold);
color: var(--color-primary);
margin-bottom: 4px;
}
.card-category {
font-size: var(--font-small);
font-weight: var(--weight-medium);
color: var(--color-accent);
background: #dbeafe;
padding: 2px 8px;
border-radius: 12px;
display: inline-block;
margin-bottom: var(--spacing-small);
width: fit-content;
}
.card-description {
font-size: var(--font-body);
color: var(--color-text);
line-height: var(--line-height-ko);
flex: 1;
}
.card-source {
font-size: var(--font-small);
color: var(--color-text-light);
font-style: italic;
margin-top: var(--spacing-small);
border-top: var(--border-width) solid var(--color-border);
padding-top: var(--spacing-small);
}
</style>
</div>
<div class="area-footer">
<!-- 결론 바 블록: 하단 핵심 한 줄 -->
<div class="block-conclusion">
<div class="conclusion-label">핵심 요약</div>
<div class="conclusion-text">BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다</div>
</div>
<style>
.block-conclusion {
background: var(--color-primary);
color: white;
padding: var(--spacing-inner) var(--spacing-block);
border-radius: var(--radius);
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
}
.conclusion-label {
font-size: var(--font-caption);
color: var(--color-text-light);
font-weight: var(--weight-medium);
text-transform: uppercase;
letter-spacing: 1px;
}
.conclusion-text {
font-size: var(--font-subtitle);
font-weight: var(--weight-bold);
line-height: var(--line-height-ko);
}
</style>
</div>
</div>
</body>
</html>

1054
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

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

View File

@@ -81,11 +81,28 @@ def _load_catalog() -> str:
DIRECTOR_PROMPT = """당신은 디자인 팀장이다. 실장이 분석한 꼭지 목록을 받아 레이아웃을 설계한다. 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} ## {catalog}
## 이미지 처리 규칙 ## 이미지 처리 규칙
@@ -99,12 +116,13 @@ DIRECTOR_PROMPT = """당신은 디자인 팀장이다. 실장이 분석한 꼭
- 공간에 안 들어가면 → 요약 요청 또는 페이지 분리 - 공간에 안 들어가면 → 요약 요청 또는 페이지 분리
## 자세히보기 규칙 ## 자세히보기 규칙
- 너무 구체적/세부적인 내용은 details-block으로 설계 - detail_target: true인 꼭지는 본문에 넣지 않는다
- 슬라이드 표면: 요약만, 펼치면: 전체 상세 - 관련된 블록 근처에 popup/링크로 연결
## 공간 배분 규칙 ## 공간 배분 규칙
- CSS grid-template-areas 형식으로 배치 - CSS grid-template-areas 형식으로 배치
- 영역명: header, left, right, center, main, footer 등 - 영역명: header, left, right, center, main, footer 등
- flow 꼭지는 좌측/메인, reference 꼭지는 우측/사이드
- 꼭지끼리 겹치지 않도록 설계 - 꼭지끼리 겹치지 않도록 설계
- 각 블록에 대략적 크기 감(small/medium/large) 제시 - 각 블록에 대략적 크기 감(small/medium/large) 제시
@@ -155,14 +173,18 @@ async def create_layout_concept(
catalog_text = _load_catalog() catalog_text = _load_catalog()
# 꼭지 요약 # 꼭지 요약 (role과 detail_target 포함)
topics_summary = [] topics_summary = []
for t in analysis.get("topics", []): for t in analysis.get("topics", []):
role = t.get("role", "flow")
line = ( line = (
f"꼭지 {t['id']}: {t['title']} " 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')}]" f"방향:{t.get('direction', '?')}, 유형:{t.get('content_type', 'text')}]"
) )
if t.get("detail_target"):
line += " → ★자세히보기 대상 (본문에 넣지 마라)"
if t.get("image_info"): if t.get("image_info"):
line += f" 이미지:{t['image_info']}" line += f" 이미지:{t['image_info']}"
if t.get("table_info"): if t.get("table_info"):
@@ -173,14 +195,21 @@ async def create_layout_concept(
system = DIRECTOR_PROMPT.replace("{catalog}", catalog_text) system = DIRECTOR_PROMPT.replace("{catalog}", catalog_text)
info_structure = analysis.get("info_structure", "정보 구조 미분석")
user_prompt = ( user_prompt = (
f"## 실장 분석 결과\n" f"## 실장 분석 결과\n"
f"제목: {analysis.get('title', '')}\n" f"제목: {analysis.get('title', '')}\n"
f"페이지 수: {analysis.get('total_pages', 1)}\n" f"페이지 수: {analysis.get('total_pages', 1)}\n"
f"정보 구조: {info_structure}\n\n"
f"꼭지 목록:\n" + "\n".join(topics_summary) + f"꼭지 목록:\n" + "\n".join(topics_summary) +
f"\n\n## 원본 콘텐츠 (분량 참고)\n{content[:2000]}\n\n" f"\n\n## 원본 콘텐츠 (분량 참고)\n{content[:2000]}\n\n"
f"## 요청\n" f"## 요청\n"
f"위 꼭지를 어떤 블록으로, 어디에, 몇 페이지로 배치할지 설계해줘.\n" f"위 꼭지를 어떤 블록으로, 어디에 배치할지 설계해줘.\n"
f"반드시 각 꼭지의 ROLE(flow/reference)에 따라 영역을 배정해라.\n"
f"flow → 좌측/메인, reference → 우측/사이드.\n"
f"detail_target → 본문에 넣지 마라.\n"
f"같은 내용이 두 블록에 중복되면 안 된다.\n"
f"텍스트는 채우지 마. 구조만 JSON으로." f"텍스트는 채우지 마. 구조만 JSON으로."
) )

View File

@@ -1,7 +1,7 @@
"""DA-12: 1단계 — Kei 실장 (꼭지 추출 + 분석). """DA-12: 1단계 — Kei 실장 (꼭지 추출 + 분석).
본문에서 핵심 꼭지를 추출하고, 각 꼭지의 레이어/강조/배치 방향을 분석한다. 1차: Kei API를 통해 Kei persona가 사고하여 꼭지를 추출한다.
이미지/표/상세 콘텐츠도 판단한다. fallback: Kei API 실패 시 Anthropic API 직접 호출.
""" """
from __future__ import annotations from __future__ import annotations
@@ -11,103 +11,191 @@ import re
from typing import Any from typing import Any
import anthropic import anthropic
import httpx
from src.config import settings from src.config import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CLASSIFICATION_PROMPT = """당신은 콘텐츠를 분석하여 슬라이드 구조를 설계하는 실장이다. KEI_PROMPT = (
"다음 콘텐츠를 슬라이드로 정리하려고 해.\n\n"
## 역할 "## 1단계: 정보 구조 파악 (꼭지 추출 전에 먼저 수행)\n"
본문에서 핵심 꼭지를 추출하고, 각 꼭지의 성격을 분석하여 슬라이드 구조를 설계한다. "- 이 콘텐츠가 하나의 흐름으로 읽히는가, 아니면 '본문 흐름''참조 정보'로 분리되는 구조인가?\n"
"- 독립적으로 참조되는 정보(용어 정의, 부록, 별도 설명)가 있는가?\n"
## 꼭지 추출 규칙 "- 상세히 다뤄야 하지만 본문 흐름을 끊는 내용(비교표, 상세 데이터)이 있는가?\n"
- 본문에서 2~5개의 핵심 꼭지(파트)를 추출한다 "- 정보 구조를 info_structure 필드에 기술해줘.\n\n"
- 1페이지 적정 꼭지 수: 5개 "## 2단계: 꼭지 추출\n"
- 꼭지가 5개를 넘고 중요도가 동등하면 → 2페이지로 분리 (의미 기반 분할) "- 1단계에서 파악한 정보 구조를 바탕으로 꼭지를 나눠줘\n"
- 5개인데 내용이 많으면 → 세부 내용은 "자세히보기" 대상으로 표시 "- 원본의 논리 흐름과 정보를 빠뜨리지 마라\n"
"- 원본에 있는 내용을 임의로 제거하거나 다른 의미로 바꾸지 마라\n"
## 각 꼭지 분석 항목 "- 슬라이드에 맞게 정리하되, 원본이 말하려는 흐름은 유지\n"
1. **레이어 수준**: 도입(문제 제기, 배경) / 핵심(핵심 내용, 정의) / 보조(사례, 근거) / 결론(요약, 핵심 메시지) "- 각 꼭지의 레이어(도입/핵심/보조/결론), 강조 여부, 배치 방향을 판단해줘\n"
2. **강조**: 눈에 띄게 해야 하는 꼭지 표시 (true/false) "- 참조 정보는 role: 'reference'로, 본문 흐름은 role: 'flow'로 표시\n"
3. **배치 방향**: 세로로 긴 내용(vertical) / 가로로 나열(horizontal) / 유연(flexible) "- 본문 흐름을 끊는 상세 내용은 detail_target: true로 표시\n"
4. **콘텐츠 유형**: text(텍스트) / image(이미지) / table(표) / mixed(혼합) "- 이미지/표가 있으면 그것도 판단해줘\n"
5. **이미지 정보** (이미지가 있는 경우): "- 1페이지 적정 꼭지: 5개. 초과 시 2페이지 분리.\n\n"
- 핵심인지 보조인지 (core/supplementary) "## 출력 형식 (JSON만)\n"
- 텍스트 포함 여부 (도표/차트는 true) "```json\n"
6. **표 정보** (표가 있는 경우): '{"title": "제목", "total_pages": 1, '
- 대략적 행/열 수 '"info_structure": "이 콘텐츠의 정보 구조 설명 (본문 흐름 vs 참조 분리 등)", '
- 전체 표시 가능한지 판단 '"topics": ['
7. **자세히보기 대상**: 너무 구체적/세부적인 내용은 detail_target: true '{"id": 1, "title": "꼭지 제목", "summary": "요약", '
'"layer": "intro|core|supporting|conclusion", '
## 출력 형식 (반드시 JSON만. 설명 없이.) '"role": "flow|reference", '
```json '"emphasis": true, "direction": "vertical|horizontal|flexible", '
{ '"content_type": "text|image|table|mixed", '
"title": "슬라이드 제목", '"detail_target": false, "page": 1}]}\n'
"total_pages": 1, "```\n\n"
"topics": [ "## 콘텐츠:\n"
{ )
"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
}
]
}
```"""
async def classify_content(content: str) -> dict[str, Any] | None: async def classify_content(content: str) -> dict[str, Any] | None:
"""1단계: 본문에서 꼭지를 추출하고 분석한다. """1단계: Kei API를 통해 꼭지를 추출하고 분석한다.
Args: 1차: Kei API (persona + RAG + 사고)
content: 원본 텍스트 콘텐츠 fallback: Anthropic API 직접 호출
Returns:
분류 결과 JSON. 실패 시 None.
""" """
if not settings.anthropic_api_key: # 1차: Kei API
logger.warning("ANTHROPIC_API_KEY 미설정. 수동 분류 모드.") 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 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: try:
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key) async with httpx.AsyncClient(timeout=None) as client:
response = await client.post(
response = await client.messages.create( f"{kei_url}/api/message",
model="claude-sonnet-4-20250514", json={
max_tokens=2048, "message": KEI_PROMPT + content,
system=CLASSIFICATION_PROMPT, "session_id": "design-agent",
messages=[ "mode": "chat",
{ },
"role": "user", timeout=None,
"content": f"다음 콘텐츠를 분석하여 꼭지를 추출하고 구조를 설계해줘:\n\n{content}",
}
],
) )
result_text = response.content[0].text if response.status_code != 200:
analysis = _parse_json(result_text) logger.warning(f"Kei API HTTP {response.status_code}")
return None
if analysis and "topics" in analysis: # SSE 응답에서 토큰 수집
logger.info( full_text = _extract_sse_text(response.text)
f"꼭지 추출 완료: {analysis.get('title', 'untitled')}, "
f"{len(analysis['topics'])}개 꼭지, " if not full_text:
f"{analysis.get('total_pages', 1)}페이지" logger.warning("Kei API 응답에서 텍스트 추출 실패")
) return None
return analysis
else: # JSON 추출
logger.warning(f"분류 JSON 파싱 실패. 응답: {result_text[:200]}") result = _parse_json(full_text)
if result and "topics" in result:
return result
logger.warning(f"Kei API JSON 파싱 실패. 텍스트: {full_text[:200]}")
return None return None
except Exception as e: 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 return None
@@ -129,7 +217,7 @@ def _parse_json(text: str) -> dict[str, Any] | None:
def manual_classify(content: str) -> dict[str, Any]: def manual_classify(content: str) -> dict[str, Any]:
"""실장 분류 실패 시 기본 구조를 반환하는 fallback.""" """분류 실패 시 기본 구조 fallback."""
return { return {
"title": "슬라이드", "title": "슬라이드",
"total_pages": 1, "total_pages": 1,

View File

@@ -10,6 +10,7 @@ from __future__ import annotations
import json import json
import logging import logging
import re
from typing import Any, AsyncIterator from typing import Any, AsyncIterator
import anthropic import anthropic

260
templates/catalog.yaml Normal file
View File

@@ -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: 자세히보기 (<details>/<summary>)