Vendor templates and prefer local template assets

This commit is contained in:
2026-04-03 08:44:55 +09:00
parent 81b6289f80
commit adef735228
80 changed files with 5077 additions and 267 deletions

View File

@@ -0,0 +1,58 @@
<!-- 원형 라벨: 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: 8px 0;
min-height: 0;
flex-shrink: 1;
}
.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);
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>

View File

@@ -0,0 +1,74 @@
<!-- 비교 박스: 이중 테두리 둥근 박스 2개 + VS -->
<!--
📋 compare-pill-pair
─────────────────
용도: 2개 개념 시각적 대비 (비교 테이블 위 헤더)
슬롯: left_label, left_sub, right_label, right_sub
Figma 원본: 이중 테두리 (바깥 얇은 선 + 안쪽 둥근 박스)
-->
<div class="block-compare-pill">
<div class="cp-item">
<div class="cp-outer">
<div class="cp-inner">
<div class="cp-label">{{ left_label }}</div>
{% if left_sub %}<div class="cp-sub">{{ left_sub }}</div>{% endif %}
</div>
</div>
</div>
<div class="cp-vs">VS</div>
<div class="cp-item">
<div class="cp-outer">
<div class="cp-inner">
<div class="cp-label">{{ right_label }}</div>
{% if right_sub %}<div class="cp-sub">{{ right_sub }}</div>{% endif %}
</div>
</div>
</div>
</div>
<style>
.block-compare-pill {
display: flex;
gap: 20px;
align-items: center;
justify-content: center;
padding: 20px 0;
}
.cp-item {
width: 350px;
}
.cp-outer {
border: 2px solid #a8d8f0;
border-radius: 60px;
padding: 6px;
background: transparent;
}
.cp-inner {
border: 2px solid #7ec8f0;
border-radius: 55px;
background: linear-gradient(135deg, #e8f4fd 0%, #d6edfa 100%);
padding: 18px 30px;
text-align: center;
}
.cp-label {
font-size: 18px;
font-weight: 800;
color: #0088cc;
line-height: 1.4;
white-space: pre-line;
}
.cp-sub {
font-size: 12px;
color: #0088cc;
margin-top: 4px;
line-height: 1.5;
white-space: pre-line;
font-weight: 500;
}
.cp-vs {
font-size: 24px;
font-weight: 800;
color: #333;
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,34 @@
<!-- 가로 흐름도: 좌→우 화살표로 연결된 단계 (SVG) -->
<!--
📋 flow-arrow-horizontal
─────────────────
용도: 발전 과정, 기술 진화, 전환 흐름 (A → B → C)
슬롯: steps[] (각 단계에 label, sub)
Figma 원본: 2-3_03 "GIS ↔ SPCC → 토공 → ..." / 2-2_04 개발 패러다임
-->
<div class="block-flow-h">
<svg viewBox="0 0 {{ steps|length * 180 }} 80" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
{% for step in steps %}
{% set x = loop.index0 * 180 + 70 %}
<rect x="{{ x - 60 }}" y="10" width="120" height="50" rx="25" fill="{{ step.color | default('#2563eb') }}" opacity="0.9" />
<text x="{{ x }}" y="32" text-anchor="middle" fill="white" font-size="13" font-weight="700">{{ step.label }}</text>
{% if step.sub %}
<text x="{{ x }}" y="48" text-anchor="middle" fill="white" font-size="10" opacity="0.8">{{ step.sub }}</text>
{% endif %}
{% if not loop.last %}
<polygon points="{{ x + 65 }},35 {{ x + 80 }},35 {{ x + 75 }},28" fill="#94a3b8" />
<polygon points="{{ x + 65 }},35 {{ x + 80 }},35 {{ x + 75 }},42" fill="#94a3b8" />
{% endif %}
{% endfor %}
</svg>
</div>
<style>
.block-flow-h {
padding: 10px 0;
overflow-x: auto;
}
.block-flow-h svg {
min-width: 400px;
}
</style>

View File

@@ -0,0 +1,56 @@
<!-- 키워드 원형 행: 원 안에 키워드 + 하단 설명 (SVG) -->
<!--
📋 keyword-circle-row
─────────────────
용도: 핵심 키워드를 원형으로 나열하며 각각 설명. 약어 풀이.
슬롯: keywords[] (각 항목에 letter, label, description, color)
Figma 원본: 2-3_03 G-S-I-M 약어 설명, 2-2_04 개발 조건 키워드
-->
<div class="block-kw-circles">
{% for kw in keywords %}
<div class="kw-item">
<svg viewBox="0 0 80 80" width="70" height="70" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="kwGrad{{ loop.index }}" cx="40%" cy="35%" r="60%">
<stop offset="0%" stop-color="{{ kw.color_light | default('#93c5fd') }}" />
<stop offset="100%" stop-color="{{ kw.color | default('#2563eb') }}" />
</radialGradient>
</defs>
<circle cx="40" cy="40" r="38" fill="url(#kwGrad{{ loop.index }})" />
<text x="40" y="44" text-anchor="middle" dominant-baseline="central" fill="white" font-size="28" font-weight="900" font-family="Pretendard Variable, sans-serif">{{ kw.letter }}</text>
</svg>
<div class="kw-label">{{ kw.label }}</div>
{% if kw.description %}<div class="kw-desc">{{ kw.description }}</div>{% endif %}
</div>
{% endfor %}
</div>
<style>
.block-kw-circles {
display: flex;
gap: 24px;
justify-content: center;
flex-wrap: wrap;
padding: 10px 0;
}
.kw-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
width: 140px;
}
.kw-label {
font-size: 14px;
font-weight: 700;
color: #1e293b;
margin-top: 8px;
}
.kw-desc {
font-size: 12px;
color: #475569;
line-height: 1.5;
margin-top: 4px;
white-space: pre-line;
}
</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,125 @@
<!-- 벤 다이어그램: SVG premium (N개 자동 배치) -->
<!--
📋 venn-diagram (P2-B: 동적 좌표 계산)
─────────────────
용도: 상위-하위 포함 관계, 기술 융합 구조
방식: renderer가 svg_calculator.prepare_venn_data()로 좌표 사전 계산
→ items[].cx, cy, r + outer_r, center_x, center_y 전달
Phase 1 fallback: outer_r이 없으면 3개 고정 좌표 사용
-->
<div class="block-venn-svg">
{% if outer_r is defined %}
{# ═══ P2-B: 동적 N개 배치 ═══ #}
<svg viewBox="0 0 {{ viewbox_width | default(600) }} {{ viewbox_height | default(550) }}" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#dce3ea" />
<stop offset="100%" stop-color="#f0f2f5" />
</linearGradient>
<radialGradient id="blueOuter" cx="45%" cy="40%" r="55%">
<stop offset="0%" stop-color="#2979ff" />
<stop offset="40%" stop-color="#1565c0" />
<stop offset="100%" stop-color="#0d47a1" />
</radialGradient>
<radialGradient id="hlGrad" cx="35%" cy="25%" r="40%">
<stop offset="0%" stop-color="rgba(255,255,255,0.5)" />
<stop offset="100%" stop-color="rgba(255,255,255,0)" />
</radialGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="8" result="blur" />
<feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
</filter>
<filter id="shadow">
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="rgba(0,0,0,0.3)" />
</filter>
{% for item in items %}
<radialGradient id="itemGrad{{ loop.index }}" cx="35%" cy="30%" r="65%">
<stop offset="0%" stop-color="{{ item.color_light | default('#93c5fd') }}" />
<stop offset="100%" stop-color="{{ item.color | default('#3b82f6') }}" />
</radialGradient>
{% endfor %}
</defs>
<rect width="{{ viewbox_width | default(600) }}" height="{{ viewbox_height | default(550) }}" fill="url(#bgGrad)" />
<circle cx="{{ center_x }}" cy="{{ center_y }}" r="{{ outer_r }}" fill="url(#blueOuter)" filter="url(#shadow)" />
<circle cx="{{ center_x }}" cy="{{ center_y }}" r="{{ outer_r - 10 }}" fill="none" stroke="#4a90d9" stroke-width="1.5" opacity="0.4" />
<circle cx="{{ center_x }}" cy="{{ center_y }}" r="{{ outer_r - 25 }}" fill="none" stroke="#5a9de0" stroke-width="1" opacity="0.3" />
<circle cx="{{ center_x }}" cy="{{ center_y }}" r="{{ outer_r - 40 }}" fill="none" stroke="#6aabe6" stroke-width="0.8" opacity="0.25" />
{% for item in items %}
<circle cx="{{ item.cx }}" cy="{{ item.cy }}" r="{{ item.r }}" fill="url(#itemGrad{{ loop.index }})" opacity="0.9" filter="url(#glow)" />
<circle cx="{{ item.cx }}" cy="{{ item.cy }}" r="{{ item.r }}" fill="url(#hlGrad)" />
<text x="{{ item.cx }}" y="{{ item.cy }}" text-anchor="middle" dominant-baseline="central" fill="white" font-size="{% if item.r > 60 %}18{% elif item.r > 40 %}15{% else %}12{% endif %}" font-weight="700">{{ item.label }}</text>
{% if item.sub %}
<text x="{{ item.cx }}" y="{{ item.cy + 18 }}" text-anchor="middle" fill="white" font-size="11" opacity="0.85">{{ item.sub }}</text>
{% endif %}
{% endfor %}
<text x="{{ center_x }}" y="{{ center_y - outer_r + 35 }}" text-anchor="middle" fill="#ffffff" font-size="13" font-weight="400" opacity="0.85">{{ center_sub | default('') }}</text>
<text x="{{ center_x }}" y="{{ center_y - outer_r + 60 }}" text-anchor="middle" fill="#ffffff" font-size="24" font-weight="900">{{ center_label }}</text>
</svg>
{% else %}
{# ═══ Phase 1 fallback: 3개 고정 ═══ #}
<svg viewBox="0 0 600 550" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#dce3ea" /><stop offset="100%" stop-color="#f0f2f5" /></linearGradient>
<radialGradient id="blueOuter" cx="45%" cy="40%" r="55%"><stop offset="0%" stop-color="#2979ff" /><stop offset="40%" stop-color="#1565c0" /><stop offset="100%" stop-color="#0d47a1" /></radialGradient>
<radialGradient id="orangeGrad" cx="35%" cy="35%" r="65%"><stop offset="0%" stop-color="#ffab40" /><stop offset="40%" stop-color="#ff6d00" /><stop offset="100%" stop-color="#c2185b" /></radialGradient>
<radialGradient id="mintGrad" cx="40%" cy="30%" r="65%"><stop offset="0%" stop-color="#80deea" /><stop offset="40%" stop-color="#26c6da" /><stop offset="100%" stop-color="#00897b" /></radialGradient>
<radialGradient id="goldGrad" cx="40%" cy="30%" r="65%"><stop offset="0%" stop-color="#ffd54f" /><stop offset="40%" stop-color="#ffb300" /><stop offset="100%" stop-color="#e65100" /></radialGradient>
<radialGradient id="hlGrad" cx="35%" cy="25%" r="40%"><stop offset="0%" stop-color="rgba(255,255,255,0.5)" /><stop offset="100%" stop-color="rgba(255,255,255,0)" /></radialGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="8" result="blur" /><feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge></filter>
<filter id="shadow"><feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="rgba(0,0,0,0.3)" /></filter>
</defs>
<rect width="600" height="550" fill="url(#bgGrad)" />
<circle cx="300" cy="270" r="230" fill="url(#blueOuter)" filter="url(#shadow)" />
<circle cx="300" cy="270" r="220" fill="none" stroke="#4a90d9" stroke-width="1.5" opacity="0.4" />
<circle cx="300" cy="270" r="205" fill="none" stroke="#5a9de0" stroke-width="1" opacity="0.3" />
<circle cx="300" cy="270" r="190" fill="none" stroke="#6aabe6" stroke-width="0.8" opacity="0.25" />
<circle cx="265" cy="300" r="120" fill="url(#orangeGrad)" opacity="0.92" filter="url(#glow)" />
<circle cx="265" cy="300" r="120" fill="url(#hlGrad)" />
<circle cx="370" cy="230" r="75" fill="url(#mintGrad)" opacity="0.9" filter="url(#glow)" />
<circle cx="370" cy="230" r="75" fill="url(#hlGrad)" />
<circle cx="365" cy="355" r="75" fill="url(#goldGrad)" opacity="0.9" filter="url(#glow)" />
<circle cx="365" cy="355" r="75" fill="url(#hlGrad)" />
<text x="300" y="95" text-anchor="middle" fill="#ffffff" font-size="13" font-weight="400" opacity="0.85">{{ center_sub | default('') }}</text>
<text x="300" y="125" text-anchor="middle" fill="#ffffff" font-size="26" font-weight="900">{{ center_label }}</text>
{% if items and items | length > 0 %}<text x="250" y="295" text-anchor="middle" fill="#ffffff" font-size="20" font-weight="800">{{ items[0].label }}</text>{% if items[0].sub %}<text x="250" y="318" text-anchor="middle" fill="#ffffff" font-size="13" opacity="0.85">{{ items[0].sub }}</text>{% endif %}{% endif %}
{% if items and items | length > 1 %}<text x="370" y="237" text-anchor="middle" fill="#ffffff" font-size="20" font-weight="800">{{ items[1].label }}</text>{% endif %}
{% if items and items | length > 2 %}<text x="365" y="362" text-anchor="middle" fill="#ffffff" font-size="20" font-weight="800">{{ items[2].label }}</text>{% endif %}
</svg>
{% endif %}
{% if description %}
<div class="venn-desc">{{ description }}</div>
{% endif %}
</div>
<style>
.block-venn-svg {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 10px 0;
flex-shrink: 1;
min-height: 0;
overflow: hidden;
}
.block-venn-svg svg {
max-width: 450px;
width: 100%;
height: auto;
flex-shrink: 1;
}
.venn-desc {
font-size: 14px;
color: #444;
text-align: center;
max-width: 450px;
line-height: 1.7;
word-break: keep-all;
}
</style>