Initial commit: Kei Design Agent

콘텐츠를 시각적으로 구조화된 슬라이드 HTML로 변환하는 독립 에이전트.

아키텍처 (4단계 파이프라인):
  1. Kei 실장 (Opus) — 콘텐츠 유형 분류 + 블록 배치
  2. 디자인 팀장 (Sonnet) — 레이아웃 컨셉 (블록 배치 + 페이지 수)
  3. 텍스트 편집자 (Sonnet) — 슬롯 텍스트 정리 (핵심 유지)
  4. CSS Grid 렌더러 — HTML 조립

블록 템플릿 7종:
  comparison, card-grid, relationship, process,
  quote-block, conclusion-bar, comparison-table

기술 스택:
  FastAPI + Anthropic API + Jinja2 + CSS Grid
  Pretendard Variable 한국어 폰트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 17:25:47 +09:00
commit c42e65fc7e
28 changed files with 3302 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
<!-- 카드 그리드 블록: 2~4열 카드 배열 -->
<div class="block-card-grid" style="--card-count: {{ cards|length }}">
{% for card in cards %}
<div class="card" style="border-top-color: {{ card.color | default('var(--color-accent)') }}">
{% if card.icon %}<div class="card-icon">{{ card.icon }}</div>{% endif %}
<div class="card-title">{{ card.title }}</div>
{% if card.category %}<span class="card-category">{{ card.category }}</span>{% endif %}
<div class="card-description">{{ card.description }}</div>
{% if card.source %}<div class="card-source">{{ card.source }}</div>{% endif %}
</div>
{% endfor %}
</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>

View File

@@ -0,0 +1,58 @@
<!-- 비교 테이블 블록: 다항목 비교 -->
<div class="block-table">
<table class="comparison-table">
<thead>
<tr>
{% for header in headers %}
<th{% if loop.first %} class="table-row-header"{% endif %}>{{ header }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for cell in row %}
<td{% if loop.first %} class="table-row-header"{% endif %}>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<style>
.block-table {
overflow: auto;
height: 100%;
display: flex;
align-items: flex-start;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
font-size: var(--font-caption);
line-height: var(--line-height-ko);
}
.comparison-table th {
background: var(--color-primary);
color: white;
font-weight: var(--weight-bold);
padding: var(--spacing-small) var(--spacing-inner);
text-align: left;
font-size: var(--font-caption);
}
.comparison-table td {
padding: var(--spacing-small) var(--spacing-inner);
border-bottom: var(--border-width) solid var(--color-border);
font-size: var(--font-caption);
vertical-align: top;
}
.comparison-table tbody tr:nth-child(even) {
background: var(--color-bg-subtle);
}
.table-row-header {
font-weight: var(--weight-bold);
color: var(--color-primary);
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,51 @@
<!-- 비교 블록: 2단 병렬 레이아웃 -->
<div class="block-comparison">
<div class="comparison-left">
<div class="comparison-header comparison-header--left">{{ left_title }}</div>
{% if left_subtitle %}<div class="comparison-subtitle">{{ left_subtitle }}</div>{% endif %}
<div class="comparison-content">{{ left_content }}</div>
</div>
<div class="comparison-divider"></div>
<div class="comparison-right">
<div class="comparison-header comparison-header--right">{{ right_title }}</div>
{% if right_subtitle %}<div class="comparison-subtitle">{{ right_subtitle }}</div>{% endif %}
<div class="comparison-content">{{ right_content }}</div>
</div>
</div>
<style>
.block-comparison {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: var(--spacing-inner);
height: 100%;
}
.comparison-divider {
width: 1px;
background: var(--color-border);
}
.comparison-header {
font-size: var(--font-subtitle);
font-weight: var(--weight-bold);
padding-bottom: var(--spacing-small);
margin-bottom: var(--spacing-small);
border-bottom: var(--accent-border) solid;
}
.comparison-header--left {
border-color: var(--color-accent);
color: var(--color-accent);
}
.comparison-header--right {
border-color: var(--color-danger);
color: var(--color-danger);
}
.comparison-subtitle {
font-size: var(--font-caption);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-small);
}
.comparison-content {
font-size: var(--font-body);
line-height: var(--line-height-ko);
}
</style>

View File

@@ -0,0 +1,31 @@
<!-- 결론 바 블록: 하단 핵심 한 줄 -->
<div class="block-conclusion">
<div class="conclusion-label">{{ label | default('핵심 요약') }}</div>
<div class="conclusion-text">{{ conclusion_text }}</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>

View File

@@ -0,0 +1,61 @@
<!-- 프로세스 블록: 가로 단계 흐름 -->
<div class="block-process">
{% for step in steps %}
<div class="process-step">
<div class="process-number">{{ step.number | default(loop.index) }}</div>
<div class="process-title">{{ step.title }}</div>
{% if step.description %}<div class="process-desc">{{ step.description }}</div>{% endif %}
</div>
{% if not loop.last %}
<div class="process-arrow"></div>
{% endif %}
{% endfor %}
</div>
<style>
.block-process {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-small);
height: 100%;
}
.process-step {
flex: 1;
text-align: center;
padding: var(--spacing-inner);
background: var(--color-bg-subtle);
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius);
}
.process-number {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--color-accent);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: var(--weight-bold);
font-size: var(--font-body);
margin: 0 auto var(--spacing-small);
}
.process-title {
font-size: var(--font-body);
font-weight: var(--weight-bold);
color: var(--color-primary);
margin-bottom: 4px;
}
.process-desc {
font-size: var(--font-caption);
color: var(--color-text-secondary);
line-height: var(--line-height-ko);
}
.process-arrow {
font-size: 1.5rem;
color: var(--color-accent);
font-weight: var(--weight-bold);
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,29 @@
<!-- 강조 인용 블록: 문제 제기, 핵심 메시지 -->
<div class="block-quote">
<div class="quote-text">{{ quote_text }}</div>
{% if source %}<div class="quote-source">{{ source }}</div>{% endif %}
</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>

View File

@@ -0,0 +1,88 @@
<!-- 관계도 블록: 벤 다이어그램 -->
<div class="block-relationship">
<div class="venn-container">
<div class="venn-outer">
<span class="venn-outer-label">{{ center_label }}</span>
<span class="venn-outer-sub">{{ center_sub }}</span>
</div>
{% for item in items %}
<div class="venn-inner venn-inner--{{ loop.index }}" style="background: {{ item.color | default('rgba(37, 99, 235, 0.8)') }}">
<span>{{ item.label }}</span>
</div>
{% endfor %}
</div>
{% if description %}
<div class="relationship-desc">
{{ description }}
</div>
{% endif %}
</div>
<style>
.block-relationship {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: var(--spacing-inner);
}
.venn-container {
position: relative;
width: 280px;
height: 280px;
}
.venn-outer {
width: 280px;
height: 280px;
border-radius: 50%;
border: 3px solid var(--color-accent);
background: rgba(37, 99, 235, 0.05);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
}
.venn-outer-label {
font-size: var(--font-subtitle);
font-weight: var(--weight-black);
color: var(--color-accent);
}
.venn-outer-sub {
font-size: var(--font-caption);
color: var(--color-text-secondary);
}
.venn-inner {
position: absolute;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: var(--font-caption);
font-weight: var(--weight-bold);
}
.venn-inner--1 {
width: 70px; height: 70px;
top: 30px; left: 30px;
background: rgba(16, 185, 129, 0.85);
}
.venn-inner--2 {
width: 80px; height: 80px;
bottom: 40px; left: 30px;
background: rgba(59, 130, 246, 0.85);
}
.venn-inner--3 {
width: 75px; height: 75px;
top: 60px; right: 25px;
background: rgba(139, 92, 246, 0.85);
}
.relationship-desc {
font-size: var(--font-body);
color: var(--color-text);
text-align: center;
max-width: 500px;
line-height: var(--line-height-ko);
}
</style>

52
templates/slide-base.html Normal file
View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>{{ slide_title | default('슬라이드') }}</title>
<link rel="stylesheet" href="/static/base.css">
<style>
{% for page in pages %}
.slide-{{ page.page_number }} {
grid-template-areas: {{ page.grid_areas }};
grid-template-columns: {{ page.grid_columns | default('1fr') }};
grid-template-rows: {{ page.grid_rows | default('auto 1fr auto') }};
}
{% for block in page.blocks %}
.slide-{{ page.page_number }} .area-{{ block.area }} {
grid-area: {{ block.area }};
}
{% endfor %}
{% endfor %}
/* 다중 페이지: 페이지 간 간격 */
.slide + .slide {
margin-top: 40px;
}
/* 인쇄 시 페이지 분리 */
@media print {
.slide {
page-break-after: always;
}
.slide + .slide {
margin-top: 0;
}
}
</style>
</head>
<body>
{% for page in pages %}
<div class="slide slide-{{ page.page_number }}">
{% if loop.first and slide_title %}
<div class="slide-title" style="grid-area: header;">{{ slide_title }}</div>
{% endif %}
{% for block in page.blocks %}
<div class="area-{{ block.area }}">
{{ block.html }}
</div>
{% endfor %}
</div>
{% endfor %}
</body>
</html>