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:
65
templates/blocks/card-grid.html
Normal file
65
templates/blocks/card-grid.html
Normal 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>
|
||||
58
templates/blocks/comparison-table.html
Normal file
58
templates/blocks/comparison-table.html
Normal 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>
|
||||
51
templates/blocks/comparison.html
Normal file
51
templates/blocks/comparison.html
Normal 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>
|
||||
31
templates/blocks/conclusion-bar.html
Normal file
31
templates/blocks/conclusion-bar.html
Normal 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>
|
||||
61
templates/blocks/process.html
Normal file
61
templates/blocks/process.html
Normal 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>
|
||||
29
templates/blocks/quote-block.html
Normal file
29
templates/blocks/quote-block.html
Normal 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>
|
||||
88
templates/blocks/relationship.html
Normal file
88
templates/blocks/relationship.html
Normal 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
52
templates/slide-base.html
Normal 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>
|
||||
Reference in New Issue
Block a user