런타임 품질 개선: Kei JSON 파싱 + 높이 예산 강제 + conclusion 강제 + FAISS 프리로드
1. kei_client.py: Kei API가 마크다운 리스트(- ) 접두사로 JSON 응답 시 전처리하여 파싱 2. image_utils.py: base_path+상대경로 이중 시 파일명 rglob 재탐색 3. design_director.py: - conclusion 꼭지 → footer zone + conclusion-accent-bar 코드 레벨 강제 - _validate_height_budget(): zone별 height_cost 합산 검증, 초과 시 큰 블록 자동 교체 - Opus 추천 프롬프트에 zone 배정 규칙 명시 (conclusion→footer 등) 4. main.py: 서버 startup 시 FAISS 인덱스 + bge-m3 모델 미리 로드 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -244,7 +244,6 @@ def _apply_defaults(blocks: list[dict[str, Any]]) -> None:
|
||||
"topic-numbered": {"number": "1", "title": "(단계)"},
|
||||
# cards/
|
||||
"card-image-3col": {"cards": []},
|
||||
"card-text-grid": {"cards": []},
|
||||
"card-dark-overlay": {"cards": []},
|
||||
"card-tag-image": {"cards": []},
|
||||
"card-icon-desc": {"cards": []},
|
||||
@@ -264,15 +263,9 @@ def _apply_defaults(blocks: list[dict[str, Any]]) -> None:
|
||||
"process-horizontal": {"steps": []},
|
||||
"flow-arrow-horizontal": {"steps": []},
|
||||
"keyword-circle-row": {"keywords": []},
|
||||
"layer-diagram": {"layers": []},
|
||||
"timeline-vertical": {"events": []},
|
||||
"timeline-horizontal": {"events": []},
|
||||
"pyramid-hierarchy": {"levels": []},
|
||||
# emphasis/
|
||||
"quote-left-border": {"quote_text": "(인용)"},
|
||||
"quote-big-mark": {"quote_text": "(인용)"},
|
||||
"quote-question": {"question": "(질문)"},
|
||||
"conclusion-accent-bar": {"conclusion_text": "(결론)"},
|
||||
"comparison-2col": {"left_title": "A", "left_content": "-", "right_title": "B", "right_content": "-"},
|
||||
"banner-gradient": {"text": "(배너)"},
|
||||
"dark-bullet-list": {"bullets": []},
|
||||
@@ -287,7 +280,6 @@ def _apply_defaults(blocks: list[dict[str, Any]]) -> None:
|
||||
"image-side-text": {"image_src": ""},
|
||||
"image-full-caption": {"src": ""},
|
||||
"image-before-after": {"before_src": "", "after_src": ""},
|
||||
"details-block": {"summary_text": "(상세 내용)", "detail_content": ""},
|
||||
}
|
||||
for block in blocks:
|
||||
if "data" not in block:
|
||||
|
||||
@@ -12,6 +12,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import anthropic
|
||||
import yaml
|
||||
|
||||
from src.config import settings
|
||||
|
||||
@@ -29,7 +30,6 @@ BLOCK_SLOTS = {
|
||||
"topic-numbered": {"required": ["number", "title"], "optional": ["description", "color"]},
|
||||
# cards/ (10개)
|
||||
"card-image-3col": {"required": ["cards"], "optional": []},
|
||||
"card-text-grid": {"required": ["cards"], "optional": []},
|
||||
"card-dark-overlay": {"required": ["cards"], "optional": []},
|
||||
"card-tag-image": {"required": ["cards"], "optional": []},
|
||||
"card-icon-desc": {"required": ["cards"], "optional": []},
|
||||
@@ -49,15 +49,9 @@ BLOCK_SLOTS = {
|
||||
"process-horizontal": {"required": ["steps"], "optional": []},
|
||||
"flow-arrow-horizontal": {"required": ["steps"], "optional": []},
|
||||
"keyword-circle-row": {"required": ["keywords"], "optional": []},
|
||||
"layer-diagram": {"required": ["layers"], "optional": ["title"]},
|
||||
"timeline-vertical": {"required": ["events"], "optional": []},
|
||||
"timeline-horizontal": {"required": ["events"], "optional": []},
|
||||
"pyramid-hierarchy": {"required": ["levels"], "optional": []},
|
||||
# emphasis/ (12개)
|
||||
"quote-left-border": {"required": ["quote_text"], "optional": ["source"]},
|
||||
"quote-big-mark": {"required": ["quote_text"], "optional": ["source"]},
|
||||
"quote-question": {"required": ["question"], "optional": ["description"]},
|
||||
"conclusion-accent-bar": {"required": ["conclusion_text"], "optional": ["label"]},
|
||||
"comparison-2col": {"required": ["left_title", "left_content", "right_title", "right_content"], "optional": ["left_subtitle", "right_subtitle"]},
|
||||
"banner-gradient": {"required": ["text"], "optional": ["sub_text"]},
|
||||
"dark-bullet-list": {"required": ["bullets"], "optional": ["title"]},
|
||||
@@ -66,7 +60,6 @@ BLOCK_SLOTS = {
|
||||
"callout-warning": {"required": ["title", "description"], "optional": ["icon"]},
|
||||
"tab-label-row": {"required": ["tabs"], "optional": []},
|
||||
"divider-text": {"required": ["text"], "optional": []},
|
||||
"details-block": {"required": ["summary_text", "detail_content"], "optional": ["label"]},
|
||||
# media/ (5개)
|
||||
"image-row-2col": {"required": ["images"], "optional": []},
|
||||
"image-grid-2x2": {"required": ["images"], "optional": []},
|
||||
@@ -306,7 +299,13 @@ async def _opus_block_recommendation(
|
||||
prompt = (
|
||||
f"슬라이드 디자인 블록 추천을 해줘.\n\n"
|
||||
f"## 프리셋: {preset_name}\n{preset['description']}\n\n"
|
||||
f"## Zone 구조\n{zone_desc}\n\n"
|
||||
f"## Zone 구조 (반드시 이 zone에 배정하라)\n{zone_desc}\n\n"
|
||||
f"## Zone 배정 규칙 (절대 규칙)\n"
|
||||
f"- flow 꼭지 → body / left / hero zone\n"
|
||||
f"- reference 꼭지 → sidebar zone\n"
|
||||
f"- conclusion 꼭지 → **반드시 footer zone** + block_type은 **conclusion-accent-bar**\n"
|
||||
f"- detail_target 꼭지 → details-block\n"
|
||||
f"- sidebar(35%)에는 시각화 블록 금지\n\n"
|
||||
f"## 꼭지 목록\n{topics_text}\n\n"
|
||||
f"## 블록 후보 (FAISS 검색 결과)\n{block_candidates}\n\n"
|
||||
f"## 요청\n"
|
||||
@@ -531,6 +530,28 @@ async def create_layout_concept(
|
||||
)
|
||||
block["area"] = default_zone
|
||||
|
||||
# 6번: conclusion 꼭지 → footer zone + conclusion-accent-bar 강제
|
||||
for block in blocks:
|
||||
topic = next(
|
||||
(t for t in analysis.get("topics", [])
|
||||
if t.get("id") == block.get("topic_id")),
|
||||
None,
|
||||
)
|
||||
if topic and topic.get("layer") == "conclusion":
|
||||
if block.get("area") != "footer":
|
||||
logger.warning(
|
||||
f"conclusion 꼭지 {block.get('topic_id')} → footer 강제 이동"
|
||||
)
|
||||
block["area"] = "footer"
|
||||
if block.get("type") != "conclusion-accent-bar":
|
||||
logger.warning(
|
||||
f"conclusion 블록 {block.get('type')} → conclusion-accent-bar 강제"
|
||||
)
|
||||
block["type"] = "conclusion-accent-bar"
|
||||
|
||||
# 5번: zone별 height_cost 합산 검증 — 초과 시 큰 블록 교체
|
||||
_validate_height_budget(blocks, preset)
|
||||
|
||||
logger.info(
|
||||
f"[Step B] 블록 매핑 완료: {preset_name}, {len(blocks)}개 블록"
|
||||
)
|
||||
@@ -550,6 +571,7 @@ async def create_layout_concept(
|
||||
logger.error(f"Step B 호출 실패: {e}", exc_info=True)
|
||||
|
||||
# fallback: 프리셋 기반 기본 배치
|
||||
# (검증 함수는 아래에 정의)
|
||||
return _fallback_layout(analysis, preset_name, preset)
|
||||
|
||||
|
||||
@@ -605,6 +627,104 @@ def _fallback_layout(
|
||||
}
|
||||
|
||||
|
||||
# height_cost → px 변환 (결정론적)
|
||||
HEIGHT_COST_PX = {
|
||||
"compact": 70,
|
||||
"medium": 150,
|
||||
"large": 250,
|
||||
"xlarge": 400,
|
||||
}
|
||||
|
||||
# xlarge/large → medium/compact 교체 후보
|
||||
DOWNGRADE_MAP = {
|
||||
"venn-diagram": "card-text-grid",
|
||||
"pyramid-hierarchy": "card-numbered",
|
||||
"card-step-vertical": "card-numbered",
|
||||
"image-grid-2x2": "image-row-2col",
|
||||
"compare-3col-badge": "comparison-2col",
|
||||
"card-image-3col": "card-text-grid",
|
||||
"card-tag-image": "card-text-grid",
|
||||
"card-compare-3col": "comparison-2col",
|
||||
"card-image-round": "card-icon-desc",
|
||||
}
|
||||
|
||||
|
||||
def _get_block_height(block_type: str) -> int:
|
||||
"""블록 타입의 height_cost를 px로 반환."""
|
||||
catalog_map = _load_catalog_map_for_height()
|
||||
cost_label = catalog_map.get(block_type, "medium")
|
||||
return HEIGHT_COST_PX.get(cost_label, 150)
|
||||
|
||||
|
||||
def _load_catalog_map_for_height() -> dict[str, str]:
|
||||
"""catalog.yaml에서 id → height_cost 매핑을 로드."""
|
||||
catalog_path = Path(__file__).parent.parent / "templates" / "catalog.yaml"
|
||||
if not catalog_path.exists():
|
||||
return {}
|
||||
try:
|
||||
with open(catalog_path, encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
return {b["id"]: b.get("height_cost", "medium") for b in data.get("blocks", [])}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _validate_height_budget(blocks: list[dict], preset: dict) -> None:
|
||||
"""zone별 height_cost 합산을 검증하고, 초과 시 큰 블록을 교체한다.
|
||||
|
||||
코드 레벨 검증 — Sonnet이 높이 예산을 안 지켜도 강제 교정.
|
||||
"""
|
||||
zones = preset.get("zones", {})
|
||||
gap_px = 20 # --spacing-block
|
||||
|
||||
# zone별 블록 그룹핑
|
||||
zone_blocks: dict[str, list[dict]] = {}
|
||||
for block in blocks:
|
||||
area = block.get("area", "body")
|
||||
if area not in zone_blocks:
|
||||
zone_blocks[area] = []
|
||||
zone_blocks[area].append(block)
|
||||
|
||||
for area, area_blocks in zone_blocks.items():
|
||||
zone_info = zones.get(area, {})
|
||||
budget = zone_info.get("budget_px", 490)
|
||||
|
||||
# 총 높이 계산
|
||||
total = sum(_get_block_height(b.get("type", "")) for b in area_blocks)
|
||||
total += gap_px * max(0, len(area_blocks) - 1)
|
||||
|
||||
if total <= budget:
|
||||
continue
|
||||
|
||||
logger.warning(
|
||||
f"[높이 예산 초과] {area}: {total}px > {budget}px. "
|
||||
f"블록: {[b.get('type') for b in area_blocks]}"
|
||||
)
|
||||
|
||||
# 가장 큰 블록부터 교체 시도
|
||||
area_blocks.sort(key=lambda b: _get_block_height(b.get("type", "")), reverse=True)
|
||||
|
||||
for block in area_blocks:
|
||||
block_type = block.get("type", "")
|
||||
block_height = _get_block_height(block_type)
|
||||
|
||||
if block_type in DOWNGRADE_MAP and block_height >= 250:
|
||||
replacement = DOWNGRADE_MAP[block_type]
|
||||
old_height = block_height
|
||||
new_height = _get_block_height(replacement)
|
||||
|
||||
block["type"] = replacement
|
||||
total = total - old_height + new_height
|
||||
|
||||
logger.info(
|
||||
f"[높이 교체] {block_type}({old_height}px) → "
|
||||
f"{replacement}({new_height}px). 잔여: {total}px/{budget}px"
|
||||
)
|
||||
|
||||
if total <= budget:
|
||||
break
|
||||
|
||||
|
||||
def _parse_json(text: str) -> dict[str, Any] | None:
|
||||
"""텍스트에서 JSON을 추출한다."""
|
||||
patterns = [
|
||||
|
||||
11
src/main.py
11
src/main.py
@@ -19,6 +19,17 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="Design Agent", version="0.1.0")
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_preload():
|
||||
"""서버 시작 시 FAISS 인덱스 + 임베딩 모델 미리 로드."""
|
||||
try:
|
||||
from src.block_search import _ensure_loaded
|
||||
_ensure_loaded()
|
||||
logger.info("FAISS 인덱스 + bge-m3 모델 미리 로드 완료")
|
||||
except Exception as e:
|
||||
logger.warning(f"FAISS 미리 로드 실패 (첫 요청 시 로드): {e}")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5174", "http://localhost:5173"],
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<!-- 카드 그리드 블록: 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>
|
||||
@@ -1,95 +0,0 @@
|
||||
<!-- 이미지 카드: 상단 이미지 + 하단 텍스트 (2~4열) -->
|
||||
<!--
|
||||
📋 card-image
|
||||
─────────────────
|
||||
용도: 단계별 설명, 카테고리별 설명 (이미지가 핵심인 카드)
|
||||
슬롯: cards[] 배열 (각 카드에 image, title, title_en, items[])
|
||||
Figma 원본: 2-1_02 > Group 1171281594 (카드 3열)
|
||||
-->
|
||||
<div class="block-card-image" style="--ci-count: {{ cards|length }}">
|
||||
{% for card in cards %}
|
||||
<div class="ci-card">
|
||||
{% if card.image %}
|
||||
<img class="ci-img" src="{{ card.image }}" alt="{{ card.title }}">
|
||||
{% endif %}
|
||||
<div class="ci-body">
|
||||
<div class="ci-title" style="color: {{ card.color | default('var(--color-accent, #006aff)') }}">{{ card.title }}</div>
|
||||
{% if card.title_en %}<div class="ci-title-en">{{ card.title_en }}</div>{% endif %}
|
||||
<div class="ci-divider"></div>
|
||||
<ul class="ci-list">
|
||||
{% for item in card.bullets %}
|
||||
<li>{{ item }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if card.source %}<div class="ci-source">{{ card.source }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-card-image {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--ci-count, 3), 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.ci-card {
|
||||
background: var(--color-bg, #ffffff);
|
||||
border-radius: var(--radius, 8px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ci-img {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
object-fit: contain;
|
||||
background: #f8f9fb;
|
||||
padding: 10px;
|
||||
}
|
||||
.ci-body {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ci-title {
|
||||
font-size: 14px;
|
||||
font-weight: var(--weight-bold, 700);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.ci-title-en {
|
||||
font-size: 12px;
|
||||
font-weight: var(--weight-normal, 400);
|
||||
color: var(--color-text-secondary, #666);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ci-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #000;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ci-list {
|
||||
list-style: disc;
|
||||
padding-left: 18px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: var(--color-text, #000);
|
||||
flex: 1;
|
||||
}
|
||||
.ci-list li {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.ci-source {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-light, #94a3b8);
|
||||
font-style: italic;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid var(--color-border, #e2e8f0);
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,56 +0,0 @@
|
||||
<!-- 원형 라벨: CSS 그라데이션 원 + 중앙 텍스트 -->
|
||||
<!--
|
||||
📋 circle-label
|
||||
─────────────────
|
||||
용도: 섹션 전환점, 핵심 키워드 강조, 시각적 구분자
|
||||
슬롯: label (필수), sub_label (선택)
|
||||
Figma 원본: 2-1_02 > Group 1171281590 (190x190)
|
||||
-->
|
||||
<div class="block-circle-label">
|
||||
<div class="cl-outer">
|
||||
<div class="cl-inner">
|
||||
<div class="cl-text">{{ label }}</div>
|
||||
{% if sub_label %}<div class="cl-sub">{{ sub_label }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-circle-label {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.cl-outer {
|
||||
width: 190px;
|
||||
height: 190px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #3db8ff 0%, #006aff 100%);
|
||||
box-shadow: 0 0 30px rgba(0, 106, 255, 0.25), 0 0 60px rgba(0, 106, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cl-inner {
|
||||
width: 170px;
|
||||
height: 170px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #4dc4ff 0%, #0080ff 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
.cl-text {
|
||||
font-size: 20px;
|
||||
font-weight: var(--weight-bold, 700);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cl-sub {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,67 +0,0 @@
|
||||
<!-- 비교 박스: 둥근 테두리 박스 2개 + VS 라벨 -->
|
||||
<!--
|
||||
📋 compare-box
|
||||
─────────────────
|
||||
용도: 2개 개념을 시각적으로 대비 (비교 테이블 위 헤더로 사용)
|
||||
슬롯: left_label, left_sub, right_label, right_sub
|
||||
Figma 원본: 2-1_02 > 하늘색 둥근 박스 2개 + 시안 텍스트
|
||||
-->
|
||||
<div class="block-compare-box">
|
||||
<div class="cb-item">
|
||||
<div class="cb-text">
|
||||
<div class="cb-label">{{ left_label }}</div>
|
||||
{% if left_sub %}<div class="cb-sub">{{ left_sub }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cb-vs">VS</div>
|
||||
<div class="cb-item">
|
||||
<div class="cb-text">
|
||||
<div class="cb-label">{{ right_label }}</div>
|
||||
{% if right_sub %}<div class="cb-sub">{{ right_sub }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-compare-box {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 15px 0;
|
||||
}
|
||||
.cb-item {
|
||||
width: 340px;
|
||||
height: 110px;
|
||||
border-radius: 55px;
|
||||
border: 3px solid #7ec8f0;
|
||||
background: linear-gradient(135deg, #e8f4fd 0%, #d4ecfa 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 10px rgba(0, 140, 220, 0.1);
|
||||
}
|
||||
.cb-text {
|
||||
text-align: center;
|
||||
}
|
||||
.cb-label {
|
||||
font-size: 19px;
|
||||
font-weight: 800;
|
||||
color: #0090d0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cb-sub {
|
||||
font-size: 13px;
|
||||
color: #0090d0;
|
||||
margin-top: 3px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-line;
|
||||
font-weight: 500;
|
||||
}
|
||||
.cb-vs {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,97 +0,0 @@
|
||||
<!-- 비교 테이블: BIM vs DX 스타일 3단 테이블 -->
|
||||
<!--
|
||||
📋 comparison-table
|
||||
─────────────────
|
||||
용도: 다항목 비교 (좌측 A | 중앙 기준 | 우측 B)
|
||||
슬롯: headers[] (3개), rows[][] (각 행 3칸)
|
||||
Figma 원본: 2-1_02 > BIM VS D/X 테이블
|
||||
특징: 중앙 칼럼에 파란 그라데이션 배지, 좌우 불릿 대비
|
||||
-->
|
||||
<div class="block-table-figma">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{% for header in headers %}
|
||||
<th class="{% if loop.index == 1 %}th-left{% elif loop.index == 2 %}th-center{% else %}th-right{% endif %}">
|
||||
{% if loop.index == 2 %}<span class="th-badge">{{ header }}</span>{% else %}{{ header }}{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
{% for cell in row %}
|
||||
<td class="{% if loop.index == 1 %}td-left{% elif loop.index == 2 %}td-center{% else %}td-right{% endif %}">{{ cell }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-table-figma {
|
||||
overflow: auto;
|
||||
}
|
||||
.block-table-figma table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* 헤더 */
|
||||
.block-table-figma thead th {
|
||||
padding: 14px 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
border-bottom: 2px solid #e8edf2;
|
||||
}
|
||||
.th-left {
|
||||
text-align: center;
|
||||
color: #6bcdff;
|
||||
}
|
||||
.th-center {
|
||||
text-align: center;
|
||||
width: 120px;
|
||||
}
|
||||
.th-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #006eff 0%, #00aaff 100%);
|
||||
color: #ffffff;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
padding: 8px 28px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
.th-right {
|
||||
text-align: center;
|
||||
color: #006eff;
|
||||
}
|
||||
|
||||
/* 본문 */
|
||||
.block-table-figma tbody td {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.td-left {
|
||||
text-align: center;
|
||||
color: #444;
|
||||
}
|
||||
.td-center {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
background: #f6f8fb;
|
||||
font-size: 13px;
|
||||
}
|
||||
.td-right {
|
||||
text-align: center;
|
||||
color: #444;
|
||||
}
|
||||
.block-table-figma tbody tr:hover {
|
||||
background: #fafbfd;
|
||||
}
|
||||
</style>
|
||||
@@ -1,51 +0,0 @@
|
||||
<!-- 비교 블록: 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>
|
||||
@@ -1,38 +0,0 @@
|
||||
<!-- 결론 바: Figma 톤에 맞춘 하단 핵심 메시지 -->
|
||||
<!--
|
||||
📋 conclusion-bar
|
||||
─────────────────
|
||||
용도: 슬라이드/페이지 하단 핵심 한 줄 요약
|
||||
슬롯: conclusion_text (필수), label (선택)
|
||||
Figma 톤: 밝은 회색 배경 + 좌측 파란 액센트 라인 + 진한 텍스트
|
||||
-->
|
||||
<div class="block-conclusion-figma">
|
||||
{% if label %}<div class="cf-label">{{ label }}</div>{% endif %}
|
||||
<div class="cf-text">{{ conclusion_text }}</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-conclusion-figma {
|
||||
background: #f4f6f9;
|
||||
border-left: 4px solid #006aff;
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 18px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.cf-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #006aff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.cf-text {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
line-height: 1.6;
|
||||
word-break: keep-all;
|
||||
}
|
||||
</style>
|
||||
@@ -1,39 +0,0 @@
|
||||
<!-- 이미지 행: 2~4장 이미지 나란히 -->
|
||||
<!--
|
||||
📋 image-row
|
||||
─────────────────
|
||||
용도: 시공 사진, 근거 자료, 현장 이미지 나란히 배치
|
||||
슬롯: images[] 배열 (각 이미지에 src, alt, caption)
|
||||
Figma 원본: 2-1_02 > image grid (460x354 x 2)
|
||||
-->
|
||||
<div class="block-image-row" style="--ir-count: {{ images|length }}">
|
||||
{% for img in images %}
|
||||
<div class="ir-item">
|
||||
<img src="{{ img.src }}" alt="{{ img.alt | default('') }}">
|
||||
{% if img.caption %}<div class="ir-caption">{{ img.caption }}</div>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-image-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--ir-count, 2), 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
.ir-item {
|
||||
overflow: hidden;
|
||||
}
|
||||
.ir-item img {
|
||||
width: 100%;
|
||||
height: 354px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.ir-caption {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-light, #94a3b8);
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +0,0 @@
|
||||
<!-- 프로세스 블록: 가로 단계 흐름 -->
|
||||
<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>
|
||||
@@ -1,29 +0,0 @@
|
||||
<!-- 강조 인용 블록: 문제 제기, 핵심 메시지 -->
|
||||
<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>
|
||||
@@ -1,88 +0,0 @@
|
||||
<!-- 관계도 블록: 벤 다이어그램 -->
|
||||
<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>
|
||||
@@ -1,69 +0,0 @@
|
||||
<!-- 섹션 타이틀: 배경 헤더 위 영문+한글 타이틀 오버레이 -->
|
||||
<!--
|
||||
📋 section-title
|
||||
─────────────────
|
||||
용도: 자세히보기 페이지 상단, 배경 이미지 위에 타이틀 표시
|
||||
슬롯: title_ko (필수), title_en (선택), breadcrumb (선택), bg_image (선택)
|
||||
Figma 원본: 공통 > section_title + bg 컴포넌트
|
||||
-->
|
||||
<div class="block-section-title">
|
||||
{% if bg_image %}
|
||||
<img class="st-bg" src="{{ bg_image }}" alt="">
|
||||
{% else %}
|
||||
<div class="st-bg st-bg-default"></div>
|
||||
{% endif %}
|
||||
{% if breadcrumb %}
|
||||
<div class="st-breadcrumb">{{ breadcrumb }}</div>
|
||||
{% endif %}
|
||||
<div class="st-text">
|
||||
{% if title_en %}<div class="st-en">{{ title_en }}</div>{% endif %}
|
||||
<div class="st-ko">{{ title_ko }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-section-title {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.st-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
z-index: 1;
|
||||
}
|
||||
.st-bg-default {
|
||||
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 50%, #4dc4ff 100%);
|
||||
}
|
||||
.st-breadcrumb {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 89px;
|
||||
z-index: 5;
|
||||
font-size: 13px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
.st-text {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
left: 89px;
|
||||
z-index: 5;
|
||||
}
|
||||
.st-en {
|
||||
font-size: 15px;
|
||||
font-weight: var(--weight-normal, 400);
|
||||
color: #ffffff;
|
||||
opacity: 0.85;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.st-ko {
|
||||
font-size: 35px;
|
||||
font-weight: var(--weight-bold, 700);
|
||||
color: #ffffff;
|
||||
line-height: 1.3;
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +0,0 @@
|
||||
<!-- 꼭지 제목+설명: 좌측 질문/소제목 + 우측 설명 -->
|
||||
<!--
|
||||
📋 topic-header
|
||||
─────────────────
|
||||
용도: 각 꼭지의 시작부, 좌측에 파란 굵은 제목 + 우측에 본문 설명
|
||||
슬롯: title (필수), description (필수)
|
||||
비율: 좌 240px : 우 나머지
|
||||
Figma 원본: sub_제목,내용 (742x68~78)
|
||||
-->
|
||||
<div class="block-topic-header">
|
||||
<div class="th-title">{{ title }}</div>
|
||||
<div class="th-desc">{{ description }}</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-topic-header {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.th-title {
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
font-size: 24px;
|
||||
font-weight: var(--weight-bold, 700);
|
||||
color: var(--color-accent-deep, #004cbe);
|
||||
line-height: 1.4;
|
||||
word-break: keep-all;
|
||||
}
|
||||
.th-desc {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: var(--weight-normal, 400);
|
||||
color: var(--color-text, #000000);
|
||||
line-height: 1.7;
|
||||
word-break: keep-all;
|
||||
}
|
||||
</style>
|
||||
@@ -1,66 +0,0 @@
|
||||
<!-- 카드 그리드 블록: 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: var(--radius);
|
||||
display: inline-block;
|
||||
margin-bottom: var(--spacing-small);
|
||||
width: fit-content;
|
||||
}
|
||||
.card-description {
|
||||
white-space: pre-line;
|
||||
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>
|
||||
@@ -1,38 +0,0 @@
|
||||
<!-- 결론 바: Figma 톤에 맞춘 하단 핵심 메시지 -->
|
||||
<!--
|
||||
📋 conclusion-bar
|
||||
─────────────────
|
||||
용도: 슬라이드/페이지 하단 핵심 한 줄 요약
|
||||
슬롯: conclusion_text (필수), label (선택)
|
||||
Figma 톤: 밝은 회색 배경 + 좌측 파란 액센트 라인 + 진한 텍스트
|
||||
-->
|
||||
<div class="block-conclusion-figma">
|
||||
{% if label %}<div class="cf-label">{{ label }}</div>{% endif %}
|
||||
<div class="cf-text">{{ conclusion_text }}</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-conclusion-figma {
|
||||
background: #f4f6f9;
|
||||
border-left: 4px solid #006aff;
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 18px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.cf-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #006aff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.cf-text {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
line-height: 1.6;
|
||||
word-break: keep-all;
|
||||
}
|
||||
</style>
|
||||
@@ -1,67 +0,0 @@
|
||||
<!-- 자세히보기: HTML 네이티브 <details>/<summary> 접기/펼치기 -->
|
||||
<!--
|
||||
📋 details-block
|
||||
─────────────────
|
||||
용도: 상세 데이터(비교표, 스펙 등)를 접어서 표시. 클릭하면 펼침.
|
||||
슬롯: summary_text (필수), detail_content (필수), label (선택)
|
||||
원칙: 본문 흐름을 끊지 않으면서 상세 정보 제공
|
||||
인쇄: window.onbeforeprint에서 자동 펼침 (slide-base.html의 JS)
|
||||
-->
|
||||
<details class="block-details">
|
||||
<summary class="dt-summary">
|
||||
{% if label %}<span class="dt-label">{{ label }}</span>{% endif %}
|
||||
<span class="dt-summary-text">{{ summary_text }}</span>
|
||||
</summary>
|
||||
<div class="dt-content">{{ detail_content }}</div>
|
||||
</details>
|
||||
|
||||
<style>
|
||||
.block-details {
|
||||
background: var(--color-bg-subtle);
|
||||
border: var(--border-width) solid var(--color-border);
|
||||
border-left: var(--accent-border) solid var(--color-accent);
|
||||
border-radius: 0 var(--radius) var(--radius) 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dt-summary {
|
||||
padding: var(--spacing-inner) var(--spacing-block);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-small);
|
||||
font-size: var(--font-body);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--color-text);
|
||||
line-height: var(--line-height-ko);
|
||||
list-style: none;
|
||||
}
|
||||
.dt-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.dt-summary::before {
|
||||
content: "▶";
|
||||
font-size: var(--font-caption);
|
||||
color: var(--color-accent);
|
||||
transition: none;
|
||||
}
|
||||
details[open] .dt-summary::before {
|
||||
content: "▼";
|
||||
}
|
||||
.dt-label {
|
||||
font-size: var(--font-caption);
|
||||
font-weight: var(--weight-bold);
|
||||
color: var(--color-accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dt-summary-text {
|
||||
word-break: keep-all;
|
||||
}
|
||||
.dt-content {
|
||||
padding: 0 var(--spacing-block) var(--spacing-inner) var(--spacing-block);
|
||||
font-size: var(--font-body);
|
||||
color: var(--color-text);
|
||||
line-height: var(--line-height-ko);
|
||||
word-break: keep-all;
|
||||
border-top: var(--border-width) solid var(--color-border);
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +0,0 @@
|
||||
<!-- 강조 인용 블록: 문제 제기, 핵심 메시지 -->
|
||||
<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 {
|
||||
white-space: pre-line;
|
||||
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>
|
||||
@@ -1,50 +0,0 @@
|
||||
<!-- 레이어 다이어그램: 겹쳐진 레이어 표현 (SVG) -->
|
||||
<!--
|
||||
📋 layer-diagram
|
||||
─────────────────
|
||||
용도: GIS/BIM/DT 레이어 구조, 기술 스택, 계층 구조 시각화
|
||||
슬롯: layers[] (각 레이어에 label, color), title (선택)
|
||||
Figma 원본: 1장_1-1_미래 "GIS+BIM+DT 레이어 시각화"
|
||||
-->
|
||||
<div class="block-layer-diag">
|
||||
{% if title %}<div class="ld-title">{{ title }}</div>{% endif %}
|
||||
<svg viewBox="0 0 400 {{ layers|length * 60 + 40 }}" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
|
||||
<defs>
|
||||
{% for layer in layers %}
|
||||
<linearGradient id="layerGrad{{ loop.index }}" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="{{ layer.color | default('#2563eb') }}" stop-opacity="0.85" />
|
||||
<stop offset="100%" stop-color="{{ layer.color | default('#2563eb') }}" stop-opacity="0.6" />
|
||||
</linearGradient>
|
||||
{% endfor %}
|
||||
<filter id="layerShadow">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.15" />
|
||||
</filter>
|
||||
</defs>
|
||||
{% for layer in layers %}
|
||||
{% set y = (layers|length - loop.index) * 55 + 20 %}
|
||||
{% set offset = loop.index0 * 15 %}
|
||||
<!-- 3D 효과: 사다리꼴 레이어 -->
|
||||
<path d="M {{ 40 + offset }},{{ y }} L {{ 360 - offset }},{{ y }} L {{ 340 - offset }},{{ y + 40 }} L {{ 60 + offset }},{{ y + 40 }} Z"
|
||||
fill="url(#layerGrad{{ loop.index }})" filter="url(#layerShadow)" />
|
||||
<text x="200" y="{{ y + 25 }}" text-anchor="middle" fill="white" font-size="14" font-weight="700">{{ layer.label }}</text>
|
||||
{% endfor %}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-layer-diag {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.ld-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
.block-layer-diag svg {
|
||||
max-width: 400px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,40 +0,0 @@
|
||||
<!-- 피라미드 계층: 위에서 아래로 넓어지는 계층 구조 (SVG) -->
|
||||
<!--
|
||||
📋 pyramid-hierarchy
|
||||
─────────────────
|
||||
용도: 위계, 우선순위, 상위→하위 개념 (좁은→넓은)
|
||||
슬롯: levels[] (상단부터, 각 레벨에 label, color)
|
||||
-->
|
||||
<div class="block-pyramid">
|
||||
<svg viewBox="0 0 500 {{ levels|length * 70 + 20 }}" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
|
||||
<defs>
|
||||
{% for level in levels %}
|
||||
<linearGradient id="pyrGrad{{ loop.index }}" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="{{ level.color | default('#2563eb') }}" />
|
||||
<stop offset="100%" stop-color="{{ level.color | default('#2563eb') }}" stop-opacity="0.7" />
|
||||
</linearGradient>
|
||||
{% endfor %}
|
||||
<filter id="pyrShadow">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.12" />
|
||||
</filter>
|
||||
</defs>
|
||||
{% for level in levels %}
|
||||
{% set i = loop.index0 %}
|
||||
{% set y = i * 65 + 10 %}
|
||||
{% set w_half = 60 + i * 55 %}
|
||||
<rect x="{{ 250 - w_half }}" y="{{ y }}" width="{{ w_half * 2 }}" height="50" rx="6" fill="url(#pyrGrad{{ loop.index }})" filter="url(#pyrShadow)" />
|
||||
<text x="250" y="{{ y + 30 }}" text-anchor="middle" fill="white" font-size="14" font-weight="700">{{ level.label }}</text>
|
||||
{% endfor %}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-pyramid {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.block-pyramid svg {
|
||||
max-width: 450px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +0,0 @@
|
||||
<!-- 가로 타임라인: 좌→우 시간축 + 마커 + 라벨 (SVG) -->
|
||||
<!--
|
||||
📋 timeline-horizontal
|
||||
─────────────────
|
||||
용도: 연도별 로드맵, 짧은 일정, 마일스톤 (가로 배치)
|
||||
슬롯: events[] (각 이벤트에 year, title, color)
|
||||
timeline-vertical과 다른 점: 가로 방향, 공간 효율적
|
||||
-->
|
||||
<div class="block-timeline-h">
|
||||
<svg viewBox="0 0 {{ events|length * 160 + 40 }} 100" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
|
||||
<!-- 가로 선 -->
|
||||
<line x1="30" y1="40" x2="{{ events|length * 160 - 10 }}" y2="40" stroke="#cbd5e1" stroke-width="2" />
|
||||
{% for event in events %}
|
||||
{% set x = loop.index0 * 160 + 60 %}
|
||||
<!-- 마커 -->
|
||||
<circle cx="{{ x }}" cy="40" r="12" fill="{{ event.color | default('#2563eb') }}" />
|
||||
<circle cx="{{ x }}" cy="40" r="5" fill="white" />
|
||||
<!-- 연도 -->
|
||||
<text x="{{ x }}" y="22" text-anchor="middle" fill="{{ event.color | default('#2563eb') }}" font-size="12" font-weight="800">{{ event.year }}</text>
|
||||
<!-- 제목 -->
|
||||
<text x="{{ x }}" y="65" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="600">{{ event.title }}</text>
|
||||
{% if event.sub %}
|
||||
<text x="{{ x }}" y="80" text-anchor="middle" fill="#64748b" font-size="10">{{ event.sub }}</text>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-timeline-h {
|
||||
padding: 10px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.block-timeline-h svg {
|
||||
min-width: 500px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,74 +0,0 @@
|
||||
<!-- 세로 타임라인: 좌측 선 + 원형 마커 + 우측 내용 (SVG 마커) -->
|
||||
<!--
|
||||
📋 timeline-vertical
|
||||
─────────────────
|
||||
용도: 연혁, 정책 시행 일정, 로드맵, 연도별 사건
|
||||
슬롯: events[] (각 이벤트에 year, title, description, color)
|
||||
Figma 참고: 정책 로드맵, 건설 정책 추진현황
|
||||
-->
|
||||
<div class="block-timeline-v">
|
||||
{% for event in events %}
|
||||
<div class="tv-event">
|
||||
<div class="tv-marker-col">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" fill="{{ event.color | default('#2563eb') }}" />
|
||||
<circle cx="12" cy="12" r="5" fill="white" />
|
||||
</svg>
|
||||
{% if not loop.last %}<div class="tv-line" style="background: {{ event.color | default('#2563eb') }}"></div>{% endif %}
|
||||
</div>
|
||||
<div class="tv-content">
|
||||
<div class="tv-year" style="color: {{ event.color | default('#2563eb') }}">{{ event.year }}</div>
|
||||
<div class="tv-title">{{ event.title }}</div>
|
||||
{% if event.description %}<div class="tv-desc">{{ event.description }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-timeline-v {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.tv-event {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
}
|
||||
.tv-marker-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
}
|
||||
.tv-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
min-height: 20px;
|
||||
opacity: 0.3;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.tv-content {
|
||||
padding-bottom: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
.tv-year {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.tv-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.tv-desc {
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
line-height: 1.7;
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user